Initial commit.
This commit is contained in:
@@ -0,0 +1,214 @@
|
|||||||
|
name: Gitea Branch PR & Ansible Deployment
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- 'main'
|
||||||
|
paths:
|
||||||
|
- '**.j2'
|
||||||
|
- '**/pr-ansible-config-deployment.yaml'
|
||||||
|
- 'ansible/**.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-and-create-pr:
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
|
name: Check and Create PR
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Install tea CLI
|
||||||
|
uses: supplypike/setup-bin@v4
|
||||||
|
with:
|
||||||
|
uri: 'https://gitea.com/gitea/tea/releases/download/v0.9.2/tea-0.9.2-linux-amd64'
|
||||||
|
name: 'tea'
|
||||||
|
version: '0.9.2'
|
||||||
|
|
||||||
|
- name: Gotify Notification
|
||||||
|
uses: eikendev/gotify-action@master
|
||||||
|
with:
|
||||||
|
gotify_api_base: '${{ secrets.RINOA_GOTIFY_URL }}'
|
||||||
|
gotify_app_token: '${{ secrets.RINOA_RUNNER_GOTIFY_TOKEN }}'
|
||||||
|
notification_title: 'GITEA: PR Check'
|
||||||
|
notification_message: 'Checking for existing PR... 🔍'
|
||||||
|
|
||||||
|
- name: Check if open PR exists
|
||||||
|
id: check-opened-pr-step
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
tea login add --name gitea-rinoa --url "${{ secrets.RINOA_GITEA_URL }}" --user gitea-sonarqube-bot --password "${{ secrets.BOT_GITEA_PASSWORD }}" --token ${{ secrets.BOT_GITEA_TOKEN }}
|
||||||
|
pr_exists=$(tea pr list --repo ${{ github.repository }} --state open --fields index,title,head | egrep '\[ANSIBLE\].*${{ github.ref_name }}' | tail -1 | wc -l)
|
||||||
|
echo "exists=$pr_exists" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create PR
|
||||||
|
if: ${{ steps.check-opened-pr-step.outputs.exists == '0' }}
|
||||||
|
run: |
|
||||||
|
tea login default gitea-rinoa
|
||||||
|
pr_index_old=$(tea pr ls --repo ${{ github.repository }} --state all --fields index,title,head --output csv | sed -e 's|"||g' | egrep '^[0-9]' | head -1 | awk -F"," '{print $1}')
|
||||||
|
pr_index_new=$(expr ${pr_index_old} + 1)
|
||||||
|
tea pr c -r ${{ github.repository }} -t "[ANSIBLE] Automated PR for ${{ github.ref_name }} - #${pr_index_new}" -d "Automatically created PR for branch: ${{ github.ref_name }}" -a ${{ github.actor }} -L "Ansible Configs.j2"
|
||||||
|
|
||||||
|
- name: Gotify Notification
|
||||||
|
uses: eikendev/gotify-action@master
|
||||||
|
with:
|
||||||
|
gotify_api_base: '${{ secrets.RINOA_GOTIFY_URL }}'
|
||||||
|
gotify_app_token: '${{ secrets.RINOA_RUNNER_GOTIFY_TOKEN }}'
|
||||||
|
notification_title: 'GITEA: PR Check'
|
||||||
|
notification_message: 'PR Created 🎟️'
|
||||||
|
|
||||||
|
ansible-dry-run:
|
||||||
|
name: Ansible Dry Run
|
||||||
|
needs: [check-and-create-pr]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
host: [rinoa, rikku, benedikta]
|
||||||
|
env:
|
||||||
|
VAULT_ADDR: ${{ secrets.RINOA_VAULT_ADDR }}
|
||||||
|
VAULT_TOKEN: ${{ secrets.VAULT_GITEA_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Cache Ansible Galaxy Collections
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ansible/collections
|
||||||
|
key: ${{ runner.os }}-ansible-${{ hashFiles('./ansible/collections/requirements.yml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-ansible-
|
||||||
|
|
||||||
|
- name: Install Ansible
|
||||||
|
uses: alex-oleshkevich/setup-ansible@v1.0.1
|
||||||
|
with:
|
||||||
|
version: "11.4.0"
|
||||||
|
|
||||||
|
- name: Install Vault & hvac
|
||||||
|
run: |
|
||||||
|
sudo apt-get update && sudo apt-get install -y unzip
|
||||||
|
curl -fsSL https://releases.hashicorp.com/vault/1.18.0/vault_1.18.0_linux_amd64.zip -o vault.zip
|
||||||
|
unzip vault.zip
|
||||||
|
sudo mv vault /usr/local/bin/
|
||||||
|
pip install hvac
|
||||||
|
|
||||||
|
- name: Gotify Notification
|
||||||
|
uses: eikendev/gotify-action@master
|
||||||
|
with:
|
||||||
|
gotify_api_base: '${{ secrets.RINOA_GOTIFY_URL }}'
|
||||||
|
gotify_app_token: '${{ secrets.RINOA_RUNNER_GOTIFY_TOKEN }}'
|
||||||
|
notification_title: 'GITEA: Ansible Dry Run'
|
||||||
|
notification_message: 'Starting dry run for ${{ matrix.host }}...'
|
||||||
|
|
||||||
|
- name: Run Ansible Dry Run
|
||||||
|
uses: dawidd6/action-ansible-playbook@v3
|
||||||
|
with:
|
||||||
|
directory: ansible/
|
||||||
|
playbook: homelab_config_deploy.yml
|
||||||
|
key: ${{ secrets.RINOA_ANSIBLE_PRIVATE_KEY }}
|
||||||
|
vault_password: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
|
||||||
|
requirements: collections/requirements.yml
|
||||||
|
options: |
|
||||||
|
--inventory inventory/hosts.yml
|
||||||
|
--limit ${{ matrix.host }}
|
||||||
|
--check
|
||||||
|
|
||||||
|
- name: Gotify Notification
|
||||||
|
uses: eikendev/gotify-action@master
|
||||||
|
with:
|
||||||
|
gotify_api_base: '${{ secrets.RINOA_GOTIFY_URL }}'
|
||||||
|
gotify_app_token: '${{ secrets.RINOA_RUNNER_GOTIFY_TOKEN }}'
|
||||||
|
notification_title: 'GITEA: Ansible Dry Run'
|
||||||
|
notification_message: 'Dry run for ${{ matrix.host }} completed.'
|
||||||
|
|
||||||
|
pr-merge:
|
||||||
|
name: PR Merge
|
||||||
|
needs: [ansible-dry-run]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Install tea
|
||||||
|
uses: supplypike/setup-bin@v4
|
||||||
|
with:
|
||||||
|
uri: 'https://gitea.com/gitea/tea/releases/download/v0.9.2/tea-0.9.2-linux-amd64'
|
||||||
|
name: 'tea'
|
||||||
|
version: '0.9.2'
|
||||||
|
|
||||||
|
- name: PR Merge
|
||||||
|
id: pr_merge
|
||||||
|
run: |
|
||||||
|
tea login add --name gitea-rinoa --url ${{ secrets.RINOA_GITEA_URL }} --user gitea-sonarqube-bot --password "${{ secrets.BOT_GITEA_PASSWORD }}" --token ${{ secrets.BOT_GITEA_TOKEN }}
|
||||||
|
tea login default gitea-rinoa
|
||||||
|
pr_index=$(tea pr ls --repo ${{ github.repository }} --state open --fields index,title,head,state --output csv | egrep ${{ github.ref_name }} | awk -F"," '{print $1}' | sed -e 's|"||g')
|
||||||
|
tea pr m --repo ${{ github.repository }} --title "Auto Merge of PR ${pr_index} - ${{ github.ref_name }}" --message "Merged by ${{ github.actor }}" ${pr_index}
|
||||||
|
echo "pr_index=${pr_index}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Gotify Notification
|
||||||
|
uses: eikendev/gotify-action@master
|
||||||
|
with:
|
||||||
|
gotify_api_base: '${{ secrets.RINOA_GOTIFY_URL }}'
|
||||||
|
gotify_app_token: '${{ secrets.RINOA_RUNNER_GOTIFY_TOKEN }}'
|
||||||
|
notification_title: 'GITEA: PR Merge Successful'
|
||||||
|
notification_message: 'PR #${{ steps.pr_merge.outputs.pr_index }} merged.'
|
||||||
|
|
||||||
|
ansible-config-deploy:
|
||||||
|
name: Ansible Config Deployment
|
||||||
|
needs: [pr-merge]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
host: [rinoa, rikku, benedikta]
|
||||||
|
env:
|
||||||
|
VAULT_ADDR: ${{ secrets.RINOA_VAULT_ADDR }}
|
||||||
|
VAULT_TOKEN: ${{ secrets.VAULT_GITEA_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Install Ansible
|
||||||
|
uses: alex-oleshkevich/setup-ansible@v1.0.1
|
||||||
|
with:
|
||||||
|
version: "11.4.0"
|
||||||
|
|
||||||
|
- name: Install Vault & hvac
|
||||||
|
run: |
|
||||||
|
sudo apt-get update && sudo apt-get install -y unzip
|
||||||
|
curl -fsSL https://releases.hashicorp.com/vault/1.18.0/vault_1.18.0_linux_amd64.zip -o vault.zip
|
||||||
|
unzip vault.zip
|
||||||
|
sudo mv vault /usr/local/bin/
|
||||||
|
pip install hvac
|
||||||
|
|
||||||
|
- name: Gotify Notification
|
||||||
|
uses: eikendev/gotify-action@master
|
||||||
|
with:
|
||||||
|
gotify_api_base: '${{ secrets.RINOA_GOTIFY_URL }}'
|
||||||
|
gotify_app_token: '${{ secrets.RINOA_RUNNER_GOTIFY_TOKEN }}'
|
||||||
|
notification_title: 'GITEA: Ansible Config Deployment'
|
||||||
|
notification_message: 'Deploying configs to ${{ matrix.host }}...'
|
||||||
|
|
||||||
|
- name: Run Ansible Config Deployment
|
||||||
|
uses: dawidd6/action-ansible-playbook@v3
|
||||||
|
with:
|
||||||
|
directory: ansible/
|
||||||
|
playbook: homelab_config_deploy.yml
|
||||||
|
key: ${{ secrets.RINOA_ANSIBLE_PRIVATE_KEY }}
|
||||||
|
vault_password: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
|
||||||
|
requirements: collections/requirements.yml
|
||||||
|
options: |
|
||||||
|
--inventory inventory/hosts.yml
|
||||||
|
--limit ${{ matrix.host }}
|
||||||
|
|
||||||
|
- name: Gotify Notification
|
||||||
|
uses: eikendev/gotify-action@master
|
||||||
|
with:
|
||||||
|
gotify_api_base: '${{ secrets.RINOA_GOTIFY_URL }}'
|
||||||
|
gotify_app_token: '${{ secrets.RINOA_RUNNER_GOTIFY_TOKEN }}'
|
||||||
|
notification_title: 'GITEA: Deployment Completed'
|
||||||
|
notification_message: 'Deployment to ${{ matrix.host }} completed successfully.'
|
||||||
+177
@@ -0,0 +1,177 @@
|
|||||||
|
.logs/*
|
||||||
|
*.retry
|
||||||
|
*.vault
|
||||||
|
collections/*
|
||||||
|
!collections/ansible_collections
|
||||||
|
!collections/requirements.yml
|
||||||
|
collections/ansible_collections/*
|
||||||
|
!collections/ansible_collections/aperature_ansible
|
||||||
|
collections/ansible_collections/aperature_ansible/*
|
||||||
|
!collections/ansible_collections/aperature_ansible/aperature
|
||||||
|
# https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Ansible
|
||||||
|
.ansible/
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[defaults]
|
||||||
|
inventory = inventory/hosts.yml
|
||||||
|
collections_paths = ./collections
|
||||||
|
host_key_checking = False
|
||||||
|
retry_files_enabled = False
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"first_boot": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
XKB_DEFAULT_MODEL=pc105
|
||||||
|
XKB_DEFAULT_LAYOUT=us
|
||||||
|
XKB_DEFAULT_VARIANT=
|
||||||
|
XKB_DEFAULT_OPTIONS=
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false,
|
||||||
|
"type": "ovos_common_play",
|
||||||
|
"preferred_audio_services": [
|
||||||
|
"mpv",
|
||||||
|
"vlc",
|
||||||
|
"simple"
|
||||||
|
],
|
||||||
|
"disable_mpris": true,
|
||||||
|
"dbus_type": "session",
|
||||||
|
"manage_external_players": false,
|
||||||
|
"active": true,
|
||||||
|
"mode": "auto"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"system_unit": "metric",
|
||||||
|
"time_format": "full",
|
||||||
|
"date_format": "DMY",
|
||||||
|
"intents": {
|
||||||
|
"persona": {
|
||||||
|
"handle_fallback": true,
|
||||||
|
"default_persona": "Benedikta"
|
||||||
|
},
|
||||||
|
"pipeline": [
|
||||||
|
"stop_high",
|
||||||
|
"converse",
|
||||||
|
"ocp_high",
|
||||||
|
"padatious_high",
|
||||||
|
"adapt_high",
|
||||||
|
"ocp_medium",
|
||||||
|
"ovos-persona-pipeline-plugin-high",
|
||||||
|
"fallback_high",
|
||||||
|
"stop_medium",
|
||||||
|
"adapt_medium",
|
||||||
|
"padatious_medium",
|
||||||
|
"fallback_medium",
|
||||||
|
"adapt_low",
|
||||||
|
"common_qa",
|
||||||
|
"ovos-persona-pipeline-plugin-low",
|
||||||
|
"fallback_low"
|
||||||
|
],
|
||||||
|
"padatious": {
|
||||||
|
"stem": false,
|
||||||
|
"cast_to_ascii": true,
|
||||||
|
"domain_engine": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tts": {
|
||||||
|
"module": "ovos-tts-plugin-server",
|
||||||
|
"ovos-tts-plugin-server": {
|
||||||
|
"voice": "alba-medium"
|
||||||
|
},
|
||||||
|
"fallback_module": "ovos-tts-plugin-piper",
|
||||||
|
"ovos-tts-plugins-piper": {
|
||||||
|
"voice": "alba-medium"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stt": {
|
||||||
|
"ovos-stt-plugin-server": {},
|
||||||
|
"module": "ovos-stt-plugin-server",
|
||||||
|
"fallback_module": "ovos-stt-plugin-fasterwhisper",
|
||||||
|
"ovos-stt-plugin-fasterwhisper": {
|
||||||
|
"model": "tiny.en"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listener": {
|
||||||
|
"wake_word": "hey_benedikta",
|
||||||
|
"VAD": {
|
||||||
|
"module": "ovos-vad-plugin-silero"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hotwords": {
|
||||||
|
"hey_benedikta": {
|
||||||
|
"module": "ovos-ww-plugin-vosk",
|
||||||
|
"listen": true,
|
||||||
|
"debug": false,
|
||||||
|
"rule": "equals",
|
||||||
|
"samples": [
|
||||||
|
"hey benedicta",
|
||||||
|
"hey benedikta",
|
||||||
|
"benedikta",
|
||||||
|
"benedicta"
|
||||||
|
],
|
||||||
|
"fallback_ww": "hey_benedikta_pocketsphinx"
|
||||||
|
},
|
||||||
|
"hey_benedikta_pocketsphinx": {
|
||||||
|
"module": "ovos-ww-plugin-pocketsphinx",
|
||||||
|
"phonemes": "HH EY . B EH N AH D IY K T AH",
|
||||||
|
"threshold": 1e-10,
|
||||||
|
"lang": "en-us",
|
||||||
|
"listen": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"system_unit": "metric",
|
||||||
|
"time_format": "full",
|
||||||
|
"date_format": "DMY",
|
||||||
|
"intents": {
|
||||||
|
"persona": {
|
||||||
|
"handle_fallback": true,
|
||||||
|
"default_persona": "Remote LLama"
|
||||||
|
},
|
||||||
|
"pipeline": [
|
||||||
|
"ocp_high",
|
||||||
|
"stop_high",
|
||||||
|
"converse",
|
||||||
|
"padatious_high",
|
||||||
|
"adapt_high",
|
||||||
|
"stop_medium",
|
||||||
|
"adapt_medium",
|
||||||
|
"ovos-persona-pipeline-plugin-high",
|
||||||
|
"fallback_medium",
|
||||||
|
"ovos-persona-pipeline-plugin-low",
|
||||||
|
"fallback_low"
|
||||||
|
],
|
||||||
|
"padatious": {
|
||||||
|
"stem": false,
|
||||||
|
"cast_to_ascii": true,
|
||||||
|
"domain_engine": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tts": {
|
||||||
|
"module": "ovos-tts-plugin-server",
|
||||||
|
"ovos-tts-plugin-server": {}
|
||||||
|
},
|
||||||
|
"stt": {
|
||||||
|
"module": "ovos-stt-plugin-server",
|
||||||
|
"fallback_module": "",
|
||||||
|
"ovos-stt-plugin-server": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,776 @@
|
|||||||
|
{
|
||||||
|
// Definition and documentation of all variables used by mycroft-core.
|
||||||
|
//
|
||||||
|
// Settings seen here are considered DEFAULT. Settings can also be
|
||||||
|
// overridden at the REMOTE level (set by the user via
|
||||||
|
// https://home.mycroft.ai), at the SYSTEM level (typically in the file
|
||||||
|
// '/etc/mycroft/mycroft.conf'), or at the USER level (typically in the
|
||||||
|
// file '~/.config/mycroft/mycroft.conf').
|
||||||
|
//
|
||||||
|
// The load order of settings is:
|
||||||
|
// DEFAULT
|
||||||
|
// REMOTE
|
||||||
|
// SYSTEM
|
||||||
|
// USER
|
||||||
|
//
|
||||||
|
// The Override: comments below indicates where these settings are generally
|
||||||
|
// set outside of this file. The load order is always followed, so an
|
||||||
|
// individual systems can still apply changes at the SYSTEM or USER levels.
|
||||||
|
|
||||||
|
// Language used for speech-to-text and text-to-speech.
|
||||||
|
// Code is a BCP-47 identifier (https://tools.ietf.org/html/bcp47), lowercased
|
||||||
|
// TODO: save unmodified, lowercase upon demand
|
||||||
|
"lang": "en-us",
|
||||||
|
|
||||||
|
// Secondary languages will also have their resource files loaded into memory
|
||||||
|
// but intents will only be considered if that lang is tagged with the utterance at STT step
|
||||||
|
"secondary_langs": [],
|
||||||
|
|
||||||
|
// Measurement units, either 'metric' or 'imperial'
|
||||||
|
"system_unit": "metric",
|
||||||
|
// Temperature units, either 'celsius' or 'fahrenheit'
|
||||||
|
"temperature_unit": "celsius",
|
||||||
|
// Windspeed units, either 'km/h', 'm/s', 'mph' or 'kn' (knots)
|
||||||
|
"windspeed_unit": "m/s",
|
||||||
|
// Precipitation units, either 'mm' or 'inch'
|
||||||
|
"precipitation_unit": "mm",
|
||||||
|
|
||||||
|
// Time format, either 'half' (e.g. "11:37 pm") or 'full' (e.g. "23:37")
|
||||||
|
"time_format": "half",
|
||||||
|
"spoken_time_format": "full",
|
||||||
|
|
||||||
|
// Date format, either 'MDY' (e.g. "11-29-1978") or 'DMY' (e.g. "29-11-1978")
|
||||||
|
"date_format": "MDY",
|
||||||
|
|
||||||
|
// Play a beep when system begins to listen?
|
||||||
|
"confirm_listening": true,
|
||||||
|
|
||||||
|
// opendata servers can be hosted by users or OVOS team, either for analyzing your usage patterns or to contribute to a open dataset
|
||||||
|
// any number of server urls can be added here, this is EXCLUSIVELY OPT-IN
|
||||||
|
// source: https://github.com/OpenVoiceOS/ovos-opendata-server
|
||||||
|
"open_data": {
|
||||||
|
// NOTE: consider enabling tigregotico.pt servers to help collecting datasets!
|
||||||
|
// this is a server hosted by @JarbasAi and will directly help improving the intent pipeline
|
||||||
|
// a dashboard is provided at https://opendata.tigregotico.pt
|
||||||
|
// more test data is needed to complement https://gitlocalize-bench.tigregotico.pt and to identity areas of improvement
|
||||||
|
"intent_urls": [
|
||||||
|
// "https://metrics.tigregotico.pt/intents"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// File locations of sounds to play for system events
|
||||||
|
"sounds": {
|
||||||
|
"start_listening": "snd/start_listening.wav",
|
||||||
|
"end_listening": "snd/end_listening.wav",
|
||||||
|
"acknowledge": "snd/acknowledge.mp3",
|
||||||
|
"error": "snd/error.mp3"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mechanism used to play WAV audio files
|
||||||
|
// by default ovos-utils will try to detect best player
|
||||||
|
//"play_wav_cmdline": "paplay %1 --stream-name=mycroft-voice",
|
||||||
|
|
||||||
|
// Mechanism used to play MP3 audio files
|
||||||
|
// by default ovos-utils will try to detect best player
|
||||||
|
//"play_mp3_cmdline": "mpg123 %1",
|
||||||
|
|
||||||
|
// Mechanism used to play OGG audio files
|
||||||
|
// by default ovos-utils will try to detect best player
|
||||||
|
//"play_ogg_cmdline": "ogg123 -q %1",
|
||||||
|
|
||||||
|
// Location where the system resides
|
||||||
|
// NOTE: Although this is set here, an Enclosure can override the value.
|
||||||
|
// For example a mycroft-core running in a car could use the GPS.
|
||||||
|
"location": {
|
||||||
|
"city": {
|
||||||
|
"code": "Lawrence",
|
||||||
|
"name": "Lawrence",
|
||||||
|
"state": {
|
||||||
|
"code": "KS",
|
||||||
|
"name": "Kansas",
|
||||||
|
"country": {
|
||||||
|
"code": "US",
|
||||||
|
"name": "United States"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"coordinate": {
|
||||||
|
"latitude": 38.971669,
|
||||||
|
"longitude": -95.23525
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"code": "America/Chicago",
|
||||||
|
"name": "Central Standard Time",
|
||||||
|
"dstOffset": 3600000,
|
||||||
|
"offset": -21600000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// default to $XDG_DATA_DIRS/mycroft
|
||||||
|
// "data_dir": "/opt/mycroft",
|
||||||
|
|
||||||
|
// by default, files persist across reboots, but be careful with space usage!
|
||||||
|
// TIP: use "/dev/shm/mycroft/cache" if you want to keep the cache in RAM or
|
||||||
|
// use "/tmp/mycroft/cache" to remove files upon reboot
|
||||||
|
// default to $XDG_DATA_DIRS/$BASE_FOLDER where BASE_FOLDER is read from ovos.conf (default "mycroft")
|
||||||
|
// "cache_path": "/tmp/mycroft/cache",
|
||||||
|
|
||||||
|
// To enable a utterance transformer plugin just add it's name with any relevant config
|
||||||
|
// these plugins can mutate the utterance between STT and the Intent stage
|
||||||
|
// they may also modify message.context with metadata
|
||||||
|
// plugins only load if they are installed and enabled in this section
|
||||||
|
"utterance_transformers": {
|
||||||
|
|
||||||
|
"ovos-utterance-normalizer": {},
|
||||||
|
|
||||||
|
// cancel utterances mid command
|
||||||
|
"ovos-utterance-plugin-cancel": {},
|
||||||
|
|
||||||
|
// define utterance fixes via fuzzy match ~/.local/share/mycroft/corrections.json
|
||||||
|
// define unconditional replacements at word level ~/.local/share/mycroft/word_corrections.json
|
||||||
|
"ovos-utterance-corrections-plugin": {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// To enable a metadata transformer plugin just add it's name with any relevant config
|
||||||
|
// these plugins can mutate the message.context between STT and the Intent stage
|
||||||
|
"metadata_transformers": {},
|
||||||
|
|
||||||
|
// Intent Pipeline / plugins config
|
||||||
|
"intents" : {
|
||||||
|
// the position in the pipeline where each engine matches can be configured in "pipeline" section below
|
||||||
|
|
||||||
|
// adjust confidence thresholds for adapt pipeline
|
||||||
|
// adapt intents with conf lower than these values will be ignored
|
||||||
|
"adapt": {
|
||||||
|
"conf_high": 0.65,
|
||||||
|
"conf_med": 0.45,
|
||||||
|
"conf_low": 0.25
|
||||||
|
},
|
||||||
|
|
||||||
|
// adjust confidence thresholds for padatious pipeline
|
||||||
|
// padatious/padacioso intents with conf lower than these values will be ignored
|
||||||
|
"padatious": {
|
||||||
|
"conf_high": 0.95,
|
||||||
|
"conf_med": 0.8,
|
||||||
|
"conf_low": 0.5,
|
||||||
|
|
||||||
|
// use snowball stemmer, helps with grammatical gender and verb tenses
|
||||||
|
"stem": false,
|
||||||
|
|
||||||
|
// normalization step to remove punctuation and punctuation
|
||||||
|
"cast_to_ascii": false,
|
||||||
|
|
||||||
|
// padaos is an internal regex matcher that ensures exact matches
|
||||||
|
"disable_padaos": false,
|
||||||
|
|
||||||
|
// domain engine will classify intents first and then the intent for that skill
|
||||||
|
// if false intents are classified directly
|
||||||
|
"domain_engine": false,
|
||||||
|
|
||||||
|
// path to save trained intent files
|
||||||
|
// by default uses XDG directories if not set
|
||||||
|
//"intent_cache": "~/.local/share/mycroft/intent_cache/",
|
||||||
|
|
||||||
|
// set to false if multithreading is wanted
|
||||||
|
"single_thread": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Common Query gathers responses to questions from various skills and chooses the best answer
|
||||||
|
"common_query": {
|
||||||
|
// maximum seconds to wait for skill responses before giving up
|
||||||
|
"max_response_wait": 6,
|
||||||
|
// how much extra seconds to allow a skill to search if it requests so
|
||||||
|
"extension_time": 3,
|
||||||
|
// reranker plugins are responsible for selecting the best answer among skill responses in case of ties
|
||||||
|
// the default is a BM25 implementation from ovos-classifiers
|
||||||
|
"reranker": "ovos-choice-solver-bm25"
|
||||||
|
},
|
||||||
|
|
||||||
|
// OVOS Common Play - handle media requests
|
||||||
|
"OCP": {
|
||||||
|
// enable usage of a pretrained classifier for MediaType matching
|
||||||
|
// if disabled a simple .voc match is used
|
||||||
|
// NOTE: classifiers currently are english only
|
||||||
|
"experimental_media_classifier": false,
|
||||||
|
"experimental_binary_classifier": false,
|
||||||
|
// legacy forces old audio service instead of OCP
|
||||||
|
"legacy": false,
|
||||||
|
// min confidence (0.0 - 1.0) to accept MediaType
|
||||||
|
"classifier_threshold": 0.4,
|
||||||
|
// min conf for each result (0 - 100)
|
||||||
|
"min_score": 40,
|
||||||
|
// filter results from "wrong" MediaType
|
||||||
|
"filter_media": true,
|
||||||
|
// filter results we lack plugins to play
|
||||||
|
"filter_SEI": true,
|
||||||
|
// playback mode
|
||||||
|
// 0 - auto
|
||||||
|
// 10 - audio results only
|
||||||
|
// 20 - video results only
|
||||||
|
"playback_mode": 0,
|
||||||
|
// if MediaType query fails, try Generic query
|
||||||
|
"search_fallback": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// the pipeline is a ordered set of frameworks to send an utterance too
|
||||||
|
// if one of the frameworks fails the next one is used, until an answer is found
|
||||||
|
// NOTE: if padatious is not installed, it will be replaced with padacioso (much slower)
|
||||||
|
// in the future these will become plugins, and new pipeline stages can be added by end users
|
||||||
|
"pipeline": [
|
||||||
|
"stop_high",
|
||||||
|
"converse",
|
||||||
|
"ocp_high",
|
||||||
|
"padatious_high",
|
||||||
|
"adapt_high",
|
||||||
|
"ocp_medium",
|
||||||
|
"fallback_high",
|
||||||
|
"stop_medium",
|
||||||
|
"adapt_medium",
|
||||||
|
"adapt_low",
|
||||||
|
"common_qa",
|
||||||
|
"fallback_medium",
|
||||||
|
"fallback_low"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// General skill values
|
||||||
|
"skills": {
|
||||||
|
|
||||||
|
// relative to "data_dir"
|
||||||
|
"directory": "skills",
|
||||||
|
|
||||||
|
// blacklisted skills to not load
|
||||||
|
// NB: This is skill_id of the skill, usually defined in the skills setup.py
|
||||||
|
"blacklisted_skills": [
|
||||||
|
// stop skill has been replaced with native core functionality
|
||||||
|
"skill-ovos-stop.openvoiceos"
|
||||||
|
],
|
||||||
|
|
||||||
|
// fallback skill configuration
|
||||||
|
"fallbacks": {
|
||||||
|
// you can add skill_id: priority to override the developer defined
|
||||||
|
// priority of those skills, this allows customization
|
||||||
|
// of unknown intent handling for default_skills + user preferences
|
||||||
|
"fallback_priorities": {
|
||||||
|
// "skill_id": 10
|
||||||
|
},
|
||||||
|
// fallback skill handling has 3 modes of operations:
|
||||||
|
// - "accept_all" # default mycroft-core behavior
|
||||||
|
// - "whitelist" # only call fallback for skills in "fallback_whitelist"
|
||||||
|
// - "blacklist" # only call fallback for skills NOT in "fallback_blacklist"
|
||||||
|
"fallback_mode": "accept_all",
|
||||||
|
"fallback_whitelist": [],
|
||||||
|
"fallback_blacklist": []
|
||||||
|
},
|
||||||
|
|
||||||
|
// converse stage configuration
|
||||||
|
"converse": {
|
||||||
|
// the default number of seconds a skill remains active
|
||||||
|
// if the user does not interact with the skill in this timespan it
|
||||||
|
// will be deactivated, default 5 minutes (same as mycroft)
|
||||||
|
"timeout": 300,
|
||||||
|
// override of "skill_timeouts" per skill_id
|
||||||
|
"skill_timeouts": {},
|
||||||
|
|
||||||
|
// conversational mode has 3 modes of operations:
|
||||||
|
// - "accept_all" # default mycroft-core behavior
|
||||||
|
// - "whitelist" # only call converse for skills in "converse_whitelist"
|
||||||
|
// - "blacklist" # only call converse for skills NOT in "converse_blacklist"
|
||||||
|
"converse_mode": "accept_all",
|
||||||
|
"converse_whitelist": [],
|
||||||
|
"converse_blacklist": [],
|
||||||
|
|
||||||
|
// converse activation has 4 modes of operations:
|
||||||
|
// - "accept_all" # default mycroft-core behavior, any skill can
|
||||||
|
// # activate itself unconditionally
|
||||||
|
// - "priority" # skills can only activate themselves if no skill with
|
||||||
|
// # higher priority is active
|
||||||
|
// - "whitelist" # only skills in "converse_whitelist" can activate themselves
|
||||||
|
// - "blacklist" # only skills NOT in converse "converse_blacklist" can activate themselves
|
||||||
|
// NOTE: this does not apply for regular skill activation, only to skill
|
||||||
|
// initiated activation requests
|
||||||
|
"converse_activation": "accept_all",
|
||||||
|
|
||||||
|
// number of consecutive times a skill is allowed to activate itself
|
||||||
|
// per minute, -1 for no limit (default), 0 to disable self-activation
|
||||||
|
"max_activations": -1,
|
||||||
|
// override of "max_activations" per skill_id
|
||||||
|
"skill_activations": {},
|
||||||
|
|
||||||
|
// if false only skills can activate themselves
|
||||||
|
// if true any skill can activate any other skill
|
||||||
|
"cross_activation": true,
|
||||||
|
|
||||||
|
// if false only skills can deactivate themselves
|
||||||
|
// if true any skill can deactivate any other skill
|
||||||
|
// NOTE: skill deactivation is not yet implemented
|
||||||
|
"cross_deactivation": true,
|
||||||
|
|
||||||
|
// you can add skill_id: priority to override the developer defined
|
||||||
|
// priority of those skills, currently there is no api for skills to
|
||||||
|
// define their default priority, it is assumed to be 50, the only current
|
||||||
|
// canonical source for converse priorities is this setting
|
||||||
|
"converse_priorities": {
|
||||||
|
// "skill_id": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
// system administrators can define different constraints in how configurations are loaded
|
||||||
|
// this is a mechanism to require root to change these config options
|
||||||
|
"system": {
|
||||||
|
// do not allow users to tamper with settings at all
|
||||||
|
"disable_user_config": false,
|
||||||
|
// do not allow remote backend to tamper with settings at all
|
||||||
|
"disable_remote_config": false,
|
||||||
|
// protected keys are individual settings that can not be changed at remote/user level
|
||||||
|
// nested dictionary keys can be defined with "key1:key2" syntax,
|
||||||
|
// eg. {"a": {"b": True, "c": False}}
|
||||||
|
// to protect "c" you would enter "a:c" in the section below
|
||||||
|
"protected_keys": {
|
||||||
|
"remote": [
|
||||||
|
"system",
|
||||||
|
"websocket",
|
||||||
|
"gui_websocket",
|
||||||
|
"network_tests",
|
||||||
|
// NOTE: selene returns listener settings as part of ww config
|
||||||
|
// they are protected because selene has no clue about your mic setup
|
||||||
|
"listener:channels",
|
||||||
|
"listener:sample_rate"
|
||||||
|
],
|
||||||
|
"user": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// The ovos-core messagebus websocket
|
||||||
|
"websocket": {
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8181,
|
||||||
|
"route": "/core",
|
||||||
|
"ssl": false,
|
||||||
|
// in mycroft-core all skills share a bus, this allows malicious skills
|
||||||
|
// to manipulate it and affect other skills, this option ensures each skill
|
||||||
|
// gets its own websocket connection
|
||||||
|
"shared_connection": true,
|
||||||
|
// any bus messages over this size in MB will be refused
|
||||||
|
"max_msg_size": 25
|
||||||
|
},
|
||||||
|
|
||||||
|
// The GUI messagebus websocket. Once port is created per connected GUI
|
||||||
|
"gui_websocket": {
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"base_port": 18181,
|
||||||
|
"route": "/gui",
|
||||||
|
"ssl": false
|
||||||
|
},
|
||||||
|
|
||||||
|
// URIs to use for testing network connection.
|
||||||
|
"network_tests": {
|
||||||
|
"ip_url": "https://api.ipify.org",
|
||||||
|
"dns_primary": "1.1.1.1",
|
||||||
|
"dns_secondary": "8.8.8.8",
|
||||||
|
"web_url": "http://nmcheck.gnome.org/check_network_status.txt",
|
||||||
|
"web_url_secondary": "https://checkonline.home-assistant.io/online.txt",
|
||||||
|
"captive_portal_url": "http://nmcheck.gnome.org/check_network_status.txt",
|
||||||
|
"captive_portal_text": "NetworkManager is online"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Settings used by the wake-up-word listener
|
||||||
|
"listener": {
|
||||||
|
"sample_rate": 16000,
|
||||||
|
|
||||||
|
// mute global audio output volume while microphone is recording
|
||||||
|
"fake_barge_in": true,
|
||||||
|
|
||||||
|
// TODO - these names are confusing, update dinkum listener to give them more descriptive names
|
||||||
|
// min speech seconds for audio to be considered speech
|
||||||
|
"speech_begin": 0.3,
|
||||||
|
// min silence seconds before speech is considered ended
|
||||||
|
"silence_end": 0.7,
|
||||||
|
// hard limit on max recording time before audio is sent to STT
|
||||||
|
"recording_timeout": 10,
|
||||||
|
// size of buffer kept for STT
|
||||||
|
"utterance_chunks_to_rewind": 2,
|
||||||
|
// sound chunks sent to ww callback (eg, saving recordings)
|
||||||
|
"wakeword_chunks_to_save": 15,
|
||||||
|
|
||||||
|
// Set 'save_path' to configure the location of files stored if
|
||||||
|
// 'record_wake_words' and/or 'save_utterances' are set to 'true'.
|
||||||
|
// WARNING: Make sure that user 'mycroft' has write-access on the
|
||||||
|
// directory!
|
||||||
|
// "save_path": "/tmp",
|
||||||
|
// Set 'record_wake_words' to save a copy of wake word triggers
|
||||||
|
// as .wav files under: /'save_path'/mycroft_wake_words
|
||||||
|
"record_wake_words": false,
|
||||||
|
// Set 'save_utterances' to save each sentence sent to STT -- by default
|
||||||
|
// they are only kept briefly in-memory. This can be useful for for
|
||||||
|
// debugging or other custom purposes. Recordings are saved
|
||||||
|
// under: /'save_path'/utterances/'utterance_filename'.wav
|
||||||
|
"save_utterances": false,
|
||||||
|
|
||||||
|
// Set 'utterance_filename' based on how you would like to save utterances.
|
||||||
|
// Special keys surrouned by curly braces will be replaces as follows
|
||||||
|
// "{md5}" - the md5 hash of the transcription
|
||||||
|
// "{uuid4}" - a random uuid4
|
||||||
|
// "{now:%Y-%m-%dT%H%M%S%z}" - the local ISO datetime
|
||||||
|
// "{utcnow:%Y-%m-%dT%H%M%S%z}" - the UTC ISO datetime
|
||||||
|
"utterance_filename": "{md5}-{uuid4}",
|
||||||
|
|
||||||
|
"wake_word_upload": {
|
||||||
|
"disable": true,
|
||||||
|
// official mycroft endpoint disabled, enable if you want to collect your own
|
||||||
|
// eg, eltocino localcroft or personal backend
|
||||||
|
"url": ""
|
||||||
|
},
|
||||||
|
|
||||||
|
// Microphone plugin to be read audio
|
||||||
|
"microphone": {
|
||||||
|
"module": "ovos-microphone-plugin-alsa",
|
||||||
|
"ovos-microphone-plugin-alsa": {"fallback_module": "ovos-microphone-plugin-sounddevice"},
|
||||||
|
"ovos-microphone-plugin-sounddevice": {"fallback_module": "ovos-microphone-plugin-pyaudio"}
|
||||||
|
},
|
||||||
|
|
||||||
|
// if true, will remove silence from both ends of audio before sending it to STT
|
||||||
|
"remove_silence": true,
|
||||||
|
|
||||||
|
// Voice Activity Detection is used to determine when speech ended
|
||||||
|
"VAD": {
|
||||||
|
// silence method defined the main vad strategy
|
||||||
|
// valid values:
|
||||||
|
// VAD_ONLY - Only use vad
|
||||||
|
// RATIO_ONLY - Only use max/current energy ratio threshold
|
||||||
|
// CURRENT_ONLY - Only use current energy threshold
|
||||||
|
// VAD_AND_RATIO - Use vad and max/current energy ratio threshold
|
||||||
|
// VAD_AND_CURRENT - Use vad and current energy threshold
|
||||||
|
// ALL - Use vad, max/current energy ratio, and current energy threshold
|
||||||
|
// NOTE: if a vad plugin is not available method will fallback to RATIO_ONLY
|
||||||
|
"silence_method": "vad_and_ratio",
|
||||||
|
// Seconds of speech before voice command has begun
|
||||||
|
"speech_seconds": 0.1,
|
||||||
|
// Seconds of silence before a voice command has finished
|
||||||
|
"silence_seconds": 0.5,
|
||||||
|
// Seconds of audio to keep before voice command has begun
|
||||||
|
"before_seconds": 0.5,
|
||||||
|
// Minimum length of voice command (seconds)
|
||||||
|
// NOTE: max_seconds uses recording_timeout listener setting
|
||||||
|
"min_seconds": 1,
|
||||||
|
// Ratio of max/current energy below which audio is considered speech
|
||||||
|
"max_current_ratio_threshold": 2,
|
||||||
|
// Energy threshold above which audio is considered speech
|
||||||
|
// NOTE: this is dynamic, only defining start value
|
||||||
|
"initial_energy_threshold": 1000.0,
|
||||||
|
|
||||||
|
// recommended plugin: "ovos-vad-plugin-silero"
|
||||||
|
"module": "ovos-vad-plugin-silero",
|
||||||
|
"ovos-vad-plugin-silero": {
|
||||||
|
"threshold": 0.2,
|
||||||
|
"fallback_module": "ovos-vad-plugin-precise"
|
||||||
|
},
|
||||||
|
"ovos-vad-plugin-precise": {
|
||||||
|
"fallback_module": "ovos-vad-plugin-webrtcvad"
|
||||||
|
},
|
||||||
|
"ovos-vad-plugin-webrtcvad": {
|
||||||
|
"vad_mode": 3,
|
||||||
|
"fallback_module": "ovos-vad-plugin-noise"
|
||||||
|
},
|
||||||
|
"ovos-vad-plugin-noise": {
|
||||||
|
"method": "all",
|
||||||
|
"max_current_ratio_threshold": 2.0,
|
||||||
|
"energy_threshold": 1000.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Override as SYSTEM or USER to select a specific microphone input instead of
|
||||||
|
// the PortAudio default input.
|
||||||
|
// "device_name": "somename", // can be regex pattern or substring
|
||||||
|
// or
|
||||||
|
// "device_index": 12,
|
||||||
|
|
||||||
|
// Retry microphone initialization infinitely on startup
|
||||||
|
"retry_mic_init" : true,
|
||||||
|
|
||||||
|
// Stop listing to the microphone during playback to prevent accidental triggering
|
||||||
|
// This is enabled by default, but instances with good microphone noise cancellation
|
||||||
|
// can disable this to listen all the time, allowing 'barge in' functionality.
|
||||||
|
"mute_during_output" : false,
|
||||||
|
|
||||||
|
// In milliseconds
|
||||||
|
"phoneme_duration": 120,
|
||||||
|
"multiplier": 1.0,
|
||||||
|
"energy_ratio": 1.5,
|
||||||
|
|
||||||
|
// NOTE, multiple hotwords are supported now, these fields define the main wake_word,
|
||||||
|
// this is equivalent to setting "active": true in the "hotwords" section below IF "active" is missing
|
||||||
|
// this field is also used to get a speakable string of main wake word, ie, mycrofts name
|
||||||
|
// this is set by selene and used in naptime skill
|
||||||
|
"wake_word": "hey_mycroft",
|
||||||
|
"stand_up_word": "wake_up",
|
||||||
|
|
||||||
|
// Settings used by microphone to set recording timeout
|
||||||
|
"recording_timeout": 10.0,
|
||||||
|
"recording_timeout_with_silence": 3.0,
|
||||||
|
|
||||||
|
// Skips all checks (eg. audio service confirmation) after the wake_word is recognized and
|
||||||
|
// instantly continues to listen for a command
|
||||||
|
"instant_listen": true,
|
||||||
|
|
||||||
|
// continuous listen is an experimental setting, it removes the need for
|
||||||
|
// wake words and uses VAD only, a streaming STT is strongly recommended
|
||||||
|
// this setting might downgrade STT accuracy depending on plugins used
|
||||||
|
"continuous_listen": false,
|
||||||
|
|
||||||
|
// hybrid listen is an experimental setting,
|
||||||
|
// it will not require a wake word for X seconds after a user interaction
|
||||||
|
// this means you dont need to say "hey mycroft" for follow up questions
|
||||||
|
// NOTE: depending on hardware this may cause mycroft to hear its own TTS responses as questions,
|
||||||
|
// in devices like the mark2 this should be safe to turn on
|
||||||
|
"hybrid_listen": false,
|
||||||
|
// number of seconds to wait for an interaction before requiring wake word again
|
||||||
|
"listen_timeout": 45
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hotword configurations
|
||||||
|
"hotwords": {
|
||||||
|
"hey_mycroft": {
|
||||||
|
"module": "ovos-ww-plugin-precise-lite",
|
||||||
|
"model": "https://github.com/OpenVoiceOS/precise-lite-models/raw/master/wakewords/en/hey_mycroft.tflite",
|
||||||
|
"expected_duration": 3,
|
||||||
|
"trigger_level": 3,
|
||||||
|
"sensitivity": 0.5,
|
||||||
|
"listen": true,
|
||||||
|
"fallback_ww": "hey_mycroft_precise"
|
||||||
|
},
|
||||||
|
// in case precise-lite is not installed, attempt to use classic precise
|
||||||
|
"hey_mycroft_precise": {
|
||||||
|
"module": "ovos-ww-plugin-precise",
|
||||||
|
"version": "0.3",
|
||||||
|
"model": "https://github.com/MycroftAI/precise-data/raw/models-dev/hey-mycroft.tar.gz",
|
||||||
|
"expected_duration": 3,
|
||||||
|
"trigger_level": 3,
|
||||||
|
"sensitivity": 0.5,
|
||||||
|
"listen": true,
|
||||||
|
"fallback_ww": "hey_mycroft_vosk"
|
||||||
|
},
|
||||||
|
// in case classic precise is not installed, attempt to use vosk
|
||||||
|
"hey_mycroft_vosk": {
|
||||||
|
"module": "ovos-ww-plugin-vosk",
|
||||||
|
"samples": ["hey mycroft", "hey microsoft", "hey mike roft", "hey minecraft"],
|
||||||
|
"rule": "fuzzy",
|
||||||
|
"listen": true,
|
||||||
|
"fallback_ww": "hey_mycroft_pocketsphinx"
|
||||||
|
},
|
||||||
|
// in case vosk is not installed, attempt to use pocketsphinx
|
||||||
|
"hey_mycroft_pocketsphinx": {
|
||||||
|
"module": "ovos-ww-plugin-pocketsphinx",
|
||||||
|
"phonemes": "HH EY . M AY K R AO F T",
|
||||||
|
"threshold": 1e-90,
|
||||||
|
"lang": "en-us",
|
||||||
|
"listen": true
|
||||||
|
},
|
||||||
|
// default wakeup word to take ovos out of SLEEPING mode,
|
||||||
|
"wake_up": {
|
||||||
|
"module": "ovos-ww-plugin-vosk",
|
||||||
|
"rule": "fuzzy",
|
||||||
|
"samples": ["wake up"],
|
||||||
|
"lang": "en-us",
|
||||||
|
// makes this a wakeup word for usage in SLEEPING mode
|
||||||
|
"wakeup": true,
|
||||||
|
"fallback_ww": "wake_up_pocketsphinx"
|
||||||
|
},
|
||||||
|
// in case vosk plugin is not installed, attempt to use pocketsphinx
|
||||||
|
"wake_up_pocketsphinx": {
|
||||||
|
"module": "ovos-ww-plugin-pocketsphinx",
|
||||||
|
"phonemes": "W EY K . AH P",
|
||||||
|
"threshold": 1e-20,
|
||||||
|
"lang": "en-us",
|
||||||
|
"wakeup": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"gui": {
|
||||||
|
// Override: SYSTEM (set by specific enclosures)
|
||||||
|
// set skill_id of initial homescreen
|
||||||
|
"idle_display_skill": "skill-ovos-homescreen.openvoiceos",
|
||||||
|
|
||||||
|
// GUI plugins / Extensions provide additional GUI platform support for specific devices
|
||||||
|
"extension": "generic",
|
||||||
|
|
||||||
|
// Generic extension can additionaly provide homescreen functionality
|
||||||
|
"generic": {
|
||||||
|
// enable/disable homescreen
|
||||||
|
"homescreen_supported": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// in headless devices you can set this flag to not emit any GUI related bus messages
|
||||||
|
// this will reduce messagebus usage
|
||||||
|
// NOTE: this may be undesired, GUI clients can also be cli based and
|
||||||
|
// do not necessarily require a desktop environment or to be running 24/7
|
||||||
|
"disable_gui": false
|
||||||
|
},
|
||||||
|
|
||||||
|
// Level of logs to store, one of "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"
|
||||||
|
// NOTE: This configuration setting is special and can only be changed in the
|
||||||
|
// SYSTEM or USER configuration file, it will not be read if defined in the
|
||||||
|
// DEFAULT (here) or in the REMOTE mycroft config.
|
||||||
|
// If not defined, the default log level is INFO.
|
||||||
|
//"log_level": "INFO",
|
||||||
|
|
||||||
|
// Messagebus types that will NOT be output to logs
|
||||||
|
// DEPRECATED: this was consumed by ovos-cli-client which is being retired
|
||||||
|
"ignore_logs": ["enclosure.mouth.viseme", "enclosure.mouth.display"],
|
||||||
|
|
||||||
|
// Settings related to remote sessions
|
||||||
|
// Overrride: none
|
||||||
|
"session": {
|
||||||
|
// Time To Live, in seconds
|
||||||
|
"ttl": -1
|
||||||
|
},
|
||||||
|
|
||||||
|
// Speech to Text parameters
|
||||||
|
"stt": {
|
||||||
|
// select a STT plugin as described in the respective readme
|
||||||
|
// ovos-stt-plugin-server default plugin has a bundled list of public whisper instances
|
||||||
|
"module": "ovos-stt-plugin-server",
|
||||||
|
// set a offline fallback STT, if your primary STT fails for some reason
|
||||||
|
"fallback_module": ""
|
||||||
|
},
|
||||||
|
|
||||||
|
// Text to Speech parameters
|
||||||
|
"tts": {
|
||||||
|
"pulse_duck": false,
|
||||||
|
// ovos tts server piper public servers by default, alan pope voice
|
||||||
|
"module": "ovos-tts-plugin-server",
|
||||||
|
"fallback_module": "",
|
||||||
|
"ovos-tts-plugin-mimic": {
|
||||||
|
"voice": "ap",
|
||||||
|
// Add every new synth to the persistent cache (not cleared)
|
||||||
|
"persist_cache": false,
|
||||||
|
// Path to the persistent cached files, default will be $XDG_DATA_DIRS/mycroft/OVOSServerTTS
|
||||||
|
// (or respective TTS name)
|
||||||
|
"preloaded_cache": "",
|
||||||
|
// Curate the /tmp/tts cache if disk usage is above min %
|
||||||
|
"min_free_percent": 75,
|
||||||
|
// How many times a utterance must be seen in order to be added to persistent cache
|
||||||
|
"persist_thresh": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// NLP plugins
|
||||||
|
// split utterances into words
|
||||||
|
// used by downstream tasks, handles lang specific nuances
|
||||||
|
"tokenization": {
|
||||||
|
// default plugin comes bundled with ovos-plugin-manager
|
||||||
|
"module": "ovos-tokenization-plugin-quebrafrases"
|
||||||
|
},
|
||||||
|
// split utterances into sub commands
|
||||||
|
// used to extract multiple intents from a single utterance
|
||||||
|
"segmentation": {
|
||||||
|
// default plugin comes bundled with ovos-plugin-manager
|
||||||
|
"module": "ovos-segmentation-plugin-quebrafrases"
|
||||||
|
},
|
||||||
|
// given an utterance extract keywords from it
|
||||||
|
// can be used to get search terms
|
||||||
|
"keyword_extract": {
|
||||||
|
// default plugin comes bundled with ovos-classifiers
|
||||||
|
"module": "ovos-keyword-extractor-heuristic"
|
||||||
|
},
|
||||||
|
// tag pronouns with the word they refer to
|
||||||
|
// used in normalization step
|
||||||
|
"coref": {
|
||||||
|
// default plugin comes bundled with ovos-classifiers
|
||||||
|
"module": "ovos-coref-solver-heuristic"
|
||||||
|
},
|
||||||
|
"postag": {
|
||||||
|
// default plugin comes bundled with ovos-classifiers
|
||||||
|
"module": "ovos-classifiers-postag-plugin"
|
||||||
|
},
|
||||||
|
// turn utterances into phoneme sequences, used to generate mouth movements
|
||||||
|
// disabled by default, only relevant in devices such as the Mark 1
|
||||||
|
"g2p": {
|
||||||
|
"module": ""
|
||||||
|
},
|
||||||
|
|
||||||
|
// DEPRECATED: this section is in the process of being fully removed,
|
||||||
|
// only providing default values for clean migration
|
||||||
|
"padatious": {
|
||||||
|
// fallback settings for padacioso (pure regex)
|
||||||
|
// set regex_only to False to re-enable padatious (ovos-core < 0.0.8)
|
||||||
|
"regex_only": false,
|
||||||
|
"fuzz": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Translation plugins
|
||||||
|
"language": {
|
||||||
|
//by default uses public servers for translation
|
||||||
|
// https://github.com/OpenVoiceOS/ovos-translate-server
|
||||||
|
"detection_module": "ovos-lang-detector-plugin-server",
|
||||||
|
"translation_module": "ovos-translate-plugin-server",
|
||||||
|
// define translate fallbacks to use if plugin can not be loaded
|
||||||
|
"ovos-translate-plugin-server": {"fallback_module": "ovos-google-translate-plugin"},
|
||||||
|
// define detect fallbacks to use if plugin can not be loaded
|
||||||
|
"ovos-lang-detector-plugin-server": {"fallback_module": "ovos-google-lang-detector-plugin"},
|
||||||
|
"ovos-google-lang-detector-plugin": {"fallback_module": "ovos-lang-detector-plugin-cld3"},
|
||||||
|
"ovos-lang-detector-plugin-cld3": {"fallback_module": "ovos-lang-detector-plugin-cld2"},
|
||||||
|
"ovos-lang-detector-plugin-cld2": {"fallback_module": "ovos-lang-detector-plugin-langdetect"},
|
||||||
|
"ovos-lang-detector-plugin-langdetect": {"fallback_module": "ovos-lang-detector-plugin-fastlang"},
|
||||||
|
"ovos-lang-detector-plugin-fastlang": {"fallback_module": "ovos-lang-detector-plugin-lingua-podre"},
|
||||||
|
"ovos-lang-detector-plugin-fastlang": {"fallback_module": "ovos-lang-detect-ngram-lm"}
|
||||||
|
},
|
||||||
|
|
||||||
|
// placeholder to help in migration to ovos-media
|
||||||
|
// if set to False the legacy audio service won't load
|
||||||
|
"enable_old_audioservice": true,
|
||||||
|
|
||||||
|
"Audio": {
|
||||||
|
// message.context may contains a source and destination
|
||||||
|
// native audio (playback / TTS) will only be played if a
|
||||||
|
// message destination is a native_source or if missing (considered a broadcast)
|
||||||
|
"native_sources": ["debug_cli", "audio", "mycroft-gui"],
|
||||||
|
|
||||||
|
// default audio player to be used by old_audioservice
|
||||||
|
// needs to be set under "backends" section, if not installed the setting is ignored without errors
|
||||||
|
// DO NOT use "OCP", that is not a valid option and only located in "backends" for legacy reasons
|
||||||
|
"default-backend": "mpv",
|
||||||
|
|
||||||
|
"backends": {
|
||||||
|
"OCP": {
|
||||||
|
// LEGACY CONFIG - OCP is in the process of being replaced by ovos-media
|
||||||
|
// if you are already using ovos-media this config does nothing
|
||||||
|
"type": "ovos_common_play",
|
||||||
|
// define order of preference for playback plugins
|
||||||
|
"preferred_audio_services": ["mpv", "vlc", "simple"],
|
||||||
|
// allow OCP to be controlled via MPRIS
|
||||||
|
"disable_mpris": true,
|
||||||
|
// dbus type for MPRIS, "session" or "system"
|
||||||
|
"dbus_type": "session",
|
||||||
|
// if MPRIS is enabled above, also allow OCP to control MPRIS enabled 3rd party applications
|
||||||
|
// voice enables them (next/prev/stop/resume..)
|
||||||
|
// and stops them when OCP starts it's own playback
|
||||||
|
// NOTE: OCP can be controlled itself via MPRIS independentely of this setting
|
||||||
|
"manage_external_players": false,
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
"simple": {
|
||||||
|
"type": "ovos_audio_simple",
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
"vlc": {
|
||||||
|
"type": "ovos_vlc",
|
||||||
|
"active": true,
|
||||||
|
// volume used during audio_ducking
|
||||||
|
"initial_volume": 100,
|
||||||
|
"low_volume": 50
|
||||||
|
},
|
||||||
|
"mpv": {
|
||||||
|
"type": "ovos_mpv",
|
||||||
|
"active": true,
|
||||||
|
// volume used during audio_ducking
|
||||||
|
"initial_volume": 100,
|
||||||
|
"low_volume": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"debug": false
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"speak_alarm": false,
|
||||||
|
"speak_timer": true,
|
||||||
|
"sound_alarm": "constant_beep.mp3",
|
||||||
|
"sound_timer": "beep4.mp3",
|
||||||
|
"snooze_mins": 15,
|
||||||
|
"timeout_min": 1,
|
||||||
|
"play_volume": 90,
|
||||||
|
"escalate_volume": true,
|
||||||
|
"priority_cutoff": 8,
|
||||||
|
"services": "",
|
||||||
|
"frequency": 15,
|
||||||
|
"sync_ask": false,
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"play_sound": true,
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"log_level": "WARNING",
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"gender": "male",
|
||||||
|
"haunted": false,
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"apiv3": "8a2e8882b465b1cf7cce9ff6b35bdd7e",
|
||||||
|
"search_depth": 5,
|
||||||
|
"match_confidence": 0.8,
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{% set vault_addr = 'https://vault.trez.wtf' %}
|
||||||
|
{% set secrets_path = 'benedikta-config/env' %}
|
||||||
|
|
||||||
|
{
|
||||||
|
"api_key": "{{ lookup('community.hashi_vault.vault_kv2_get', 'env', engine_mount_point='benedikta-config', url=vault_addr, token=vault_token_cleaned)['secret']['HOME_ASSISTANT_LONG_LIVED_TOKEN'] }}",
|
||||||
|
"host": "192.168.1.250:8123",
|
||||||
|
"__mycroft_skill_firstrun": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"location": {
|
||||||
|
"city": {
|
||||||
|
"code": "Queens",
|
||||||
|
"name": "Queens",
|
||||||
|
"state": {
|
||||||
|
"code": "NY",
|
||||||
|
"name": "New York",
|
||||||
|
"country": {
|
||||||
|
"code": "US",
|
||||||
|
"name": "United States"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"coordinate": {
|
||||||
|
"latitude": 40.7513,
|
||||||
|
"longitude": -73.8244
|
||||||
|
},
|
||||||
|
"timezone": {
|
||||||
|
"code": "America/New_York",
|
||||||
|
"name": "America/New_York"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "Benedikta",
|
||||||
|
"solvers": [
|
||||||
|
"ovos-solver-openai-persona-plugin",
|
||||||
|
"ovos-wikipedia-solver",
|
||||||
|
"ovos-skill-ddg",
|
||||||
|
"ovos-wolfram-alpha-solver",
|
||||||
|
"ovos-skill-wordnet",
|
||||||
|
"ovos-solver-failure-plugin"
|
||||||
|
],
|
||||||
|
"ovos-solver-openai-persona-plugin": {
|
||||||
|
"api_url": "http://192.168.1.254:11434/api",
|
||||||
|
"persona": "helpful, creative, sarcastic, sassy, clever, and very friendly."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Phi4",
|
||||||
|
"solvers": [
|
||||||
|
"ovos-solver-openai-plugin",
|
||||||
|
"ovos-solver-failure-plugin"
|
||||||
|
],
|
||||||
|
"ovos-solver-openai-plugin": {
|
||||||
|
"api_url": "https://phi4.tigregotico.pt",
|
||||||
|
"key": "sk-xxx",
|
||||||
|
"model": "phi4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Qwen",
|
||||||
|
"solvers": [
|
||||||
|
"ovos-solver-openai-plugin",
|
||||||
|
"ovos-solver-failure-plugin"
|
||||||
|
],
|
||||||
|
"ovos-solver-openai-plugin": {
|
||||||
|
"api_url": "https://qwen.tigregotico.pt",
|
||||||
|
"key": "sk-xxx",
|
||||||
|
"model": "qwen2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "Salamandra",
|
||||||
|
"solvers": [
|
||||||
|
"ovos-solver-openai-persona-plugin"
|
||||||
|
],
|
||||||
|
"ovos-solver-openai-persona-plugin": {
|
||||||
|
"api_url": "https://salamandra.tigregotico.pt",
|
||||||
|
"key": "sk-xxxx",
|
||||||
|
"persona": "helpful, creative, clever, and very friendly."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
ovos-adapt-parser==1.0.8
|
||||||
|
ovos-audio==1.0.2a1
|
||||||
|
ovos-audio-plugin-mpv==0.2.1
|
||||||
|
ovos-audio-plugin-simple==0.1.2
|
||||||
|
ovos-audio-transformer-plugin-ggwave==0.0.5
|
||||||
|
ovos-bidirectional-translation-plugin==0.1.2
|
||||||
|
ovos-bus-client==1.3.5a1
|
||||||
|
ovos-common-query-pipeline-plugin==1.1.8
|
||||||
|
ovos-config==2.1.2a3
|
||||||
|
ovos-core==2.0.4a4
|
||||||
|
ovos-date-parser==0.6.3
|
||||||
|
ovos-dialog-normalizer-plugin==0.0.1
|
||||||
|
ovos-dinkum-listener==0.4.2a3
|
||||||
|
ovos-docs-viewer==0.0.1
|
||||||
|
ovos-gui==1.3.4a1
|
||||||
|
ovos-lang-parser==0.0.3a1
|
||||||
|
ovos-m2v-pipeline==0.0.6
|
||||||
|
ovos-mark1-utils==0.0.1
|
||||||
|
ovos-media-plugin-chromecast==0.1.2
|
||||||
|
ovos-media-plugin-spotify==0.2.6
|
||||||
|
ovos-microphone-plugin-alsa==0.1.2
|
||||||
|
ovos-microphone-plugin-sounddevice==0.0.1
|
||||||
|
ovos-number-parser==0.4.2
|
||||||
|
ovos-ocp-files-plugin==0.13.1
|
||||||
|
ovos-ocp-m3u-plugin==0.0.2a2
|
||||||
|
ovos-ocp-news-plugin==0.1.1
|
||||||
|
ovos-ocp-pipeline-plugin==1.1.16
|
||||||
|
ovos-ocp-rss-plugin==0.1.1
|
||||||
|
ovos-ocp-youtube-plugin==0.0.5
|
||||||
|
ovos-openai-plugin==2.0.3
|
||||||
|
ovos-padatious==1.4.2
|
||||||
|
ovos-persona==0.6.23
|
||||||
|
ovos-phal==0.2.10
|
||||||
|
ovos-phal-plugin-alsa==0.1.4
|
||||||
|
ovos-phal-plugin-balena-wifi==1.2.2
|
||||||
|
ovos-phal-plugin-camera==0.2.1
|
||||||
|
ovos-phal-plugin-connectivity-events==0.1.2
|
||||||
|
ovos-phal-plugin-ipgeo==0.1.6
|
||||||
|
ovos-phal-plugin-mk1==0.1.3
|
||||||
|
ovos-phal-plugin-network-manager==1.0.1a2
|
||||||
|
ovos-phal-plugin-oauth==0.1.3
|
||||||
|
ovos-phal-plugin-system==1.3.4a1
|
||||||
|
ovos-phal-plugin-wallpaper-manager==0.2.5
|
||||||
|
ovos-phal-plugin-wifi-setup==1.1.1a1
|
||||||
|
ovos-plugin-common-play==1.2.1
|
||||||
|
ovos-plugin-manager==1.0.4a2
|
||||||
|
ovos-skill-alerts==0.1.27
|
||||||
|
ovos-skill-audio-recording==0.2.7
|
||||||
|
ovos-skill-boot-finished==0.5.0
|
||||||
|
ovos-skill-camera==1.0.5a3
|
||||||
|
ovos-skill-config-tool==0.7.0
|
||||||
|
ovos-skill-confucius-quotes==0.1.13
|
||||||
|
ovos-skill-date-time==1.1.4
|
||||||
|
ovos-skill-days-in-history==0.3.11
|
||||||
|
ovos-skill-ddg==0.3.6a1
|
||||||
|
ovos-skill-diagnostics==0.0.8
|
||||||
|
ovos-skill-dictation==0.2.19
|
||||||
|
ovos-skill-fallback-unknown==0.1.9
|
||||||
|
ovos-skill-hello-world==0.2.3a2
|
||||||
|
ovos-skill-icanhazdadjokes==0.3.7
|
||||||
|
ovos-skill-ip==0.2.8
|
||||||
|
ovos-skill-iss-location==0.2.16
|
||||||
|
ovos-skill-laugh==0.2.3
|
||||||
|
ovos-skill-local-media==0.2.12
|
||||||
|
ovos-skill-moviemaster==0.0.12
|
||||||
|
ovos-skill-naptime==0.3.15
|
||||||
|
ovos-skill-news==0.4.5
|
||||||
|
ovos-skill-number-facts==0.1.12
|
||||||
|
ovos-skill-parrot==0.1.25
|
||||||
|
ovos-skill-personal==0.1.19
|
||||||
|
ovos-skill-pyradios==0.1.5
|
||||||
|
ovos-skill-randomness==0.1.2
|
||||||
|
ovos-skill-somafm==0.1.6a1
|
||||||
|
ovos-skill-speedtest==0.3.6
|
||||||
|
ovos-skill-spelling==0.2.6
|
||||||
|
ovos-skill-volume==0.1.16
|
||||||
|
ovos-skill-weather==1.0.5a1
|
||||||
|
ovos-skill-wikihow==0.3.3
|
||||||
|
ovos-skill-wikipedia==0.8.13
|
||||||
|
ovos-skill-wolfie==0.5.8
|
||||||
|
ovos-skill-word-of-the-day==0.2.0
|
||||||
|
ovos-skill-wordnet==0.2.6a1
|
||||||
|
ovos-skill-youtube-music==0.1.7
|
||||||
|
ovos-solver-bm25-plugin==0.0.1
|
||||||
|
ovos-solver-failure-plugin==0.0.2
|
||||||
|
ovos-solver-yes-no-plugin==0.2.9a3
|
||||||
|
ovos-stt-plugin-chromium==0.1.2
|
||||||
|
ovos-stt-plugin-fasterwhisper==0.0.1a8
|
||||||
|
ovos-stt-plugin-server==0.1.2
|
||||||
|
ovos-translate-server-plugin==0.0.4
|
||||||
|
ovos-tts-plugin-server==0.0.4
|
||||||
|
ovos-utils==0.8.1
|
||||||
|
ovos-utterance-corrections-plugin==0.1.1
|
||||||
|
ovos-utterance-normalizer==0.2.2
|
||||||
|
ovos-utterance-plugin-cancel==0.2.4
|
||||||
|
ovos-vad-plugin-noise==0.1.2
|
||||||
|
ovos-vad-plugin-silero==0.0.5
|
||||||
|
ovos-wikipedia-solver==0.1.1
|
||||||
|
ovos-wolfram-alpha-solver==0.0.3
|
||||||
|
ovos-workshop==7.0.9a1
|
||||||
|
ovos-ww-plugin-openwakeword==0.4.1
|
||||||
|
ovos-ww-plugin-precise-lite==0.1.6
|
||||||
|
ovos-ww-plugin-vosk==0.1.7
|
||||||
|
ovos-yaml-editor==0.0.2
|
||||||
|
skill-homeassistant==0.5.1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/gmrender.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/ovos-audio.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/ovos-ggwave.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/ovos-gui.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/ovos-listener.service
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/ovos-messagebus.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/ovos-phal.service
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/ovos-skill-settings-ui.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/ovos-skills.service
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/ovos-yaml-editor.service
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/ovos/.config/systemd/user/ovos.service
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=DLNA Renderer
|
||||||
|
Wants=network-online.target sound.target
|
||||||
|
After=network-online.target sound.target
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStartPre=/bin/sleep 15
|
||||||
|
ExecStart=/usr/bin/gmediarender -f "RaspOVOS" --gstout-audiosink=pipewiresink --logfile=stdout --mime-filter audio
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS Audio
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
After=ovos-messagebus.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
Group=ovos
|
||||||
|
UMask=002
|
||||||
|
ExecStart=%h/.venvs/ovos/bin/python /usr/libexec/ovos-systemd-audio
|
||||||
|
TimeoutStartSec=10m
|
||||||
|
TimeoutStopSec=1m
|
||||||
|
Restart=on-failure
|
||||||
|
StartLimitInterval=5min
|
||||||
|
StartLimitBurst=4
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Documentation=https://github.com/OpenVoiceOS/ovos-audio-transformer-plugin-ggwave
|
||||||
|
Description=Open Voice OS - ggwave listener
|
||||||
|
PartOf=ovos.service
|
||||||
|
Requires=ovos.service ovos-messagebus.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Group=ovos
|
||||||
|
UMask=002
|
||||||
|
WorkingDirectory=%h/.venvs/ovos
|
||||||
|
ExecStart=%h/.venvs/ovos/bin/ovos-ggwave-listener
|
||||||
|
ExecReload=/usr/bin/kill -s HUP $MAINPID
|
||||||
|
ExecStop=/usr/bin/kill -s KILL $MAINPID
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
StartLimitBurst=0
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS GUI Websocket
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
After=ovos-messagebus.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
Group=ovos
|
||||||
|
UMask=002
|
||||||
|
ExecStart=%h/.venvs/ovos/bin/python /usr/libexec/ovos-systemd-gui
|
||||||
|
TimeoutStartSec=1m
|
||||||
|
TimeoutStopSec=1m
|
||||||
|
Restart=on-failure
|
||||||
|
StartLimitInterval=5min
|
||||||
|
StartLimitBurst=4
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS Librespot OCP Hooks
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
After=ovos-messagebus.service
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Group=ovos
|
||||||
|
UMask=002
|
||||||
|
ExecStart=/usr/bin/librespot --name raspOVOS --device-type "speaker" --initial-volume 100 --onevent "/usr/libexec/ovos-librespot"
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS Listener
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
After=ovos-messagebus.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
Group=ovos
|
||||||
|
UMask=002
|
||||||
|
ExecStart=%h/.venvs/ovos/bin/python /usr/libexec/ovos-systemd-listener
|
||||||
|
TimeoutStartSec=5m
|
||||||
|
TimeoutStopSec=1m
|
||||||
|
Restart=on-failure
|
||||||
|
StartLimitInterval=5min
|
||||||
|
StartLimitBurst=4
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS Messagebus (Rust)
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Group=ovos
|
||||||
|
UMask=002
|
||||||
|
ExecStart=/usr/local/bin/ovos_rust_messagebus
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS PHAL
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
After=ovos-messagebus.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Group=ovos
|
||||||
|
Type=notify
|
||||||
|
UMask=002
|
||||||
|
ExecStart=%h/.venvs/ovos/bin/python /usr/libexec/ovos-systemd-phal
|
||||||
|
TimeoutStartSec=1m
|
||||||
|
TimeoutStopSec=1m
|
||||||
|
Restart=on-failure
|
||||||
|
StartLimitInterval=5min
|
||||||
|
StartLimitBurst=4
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS Skill Settings Editor
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Group=ovos
|
||||||
|
UMask=002
|
||||||
|
Environment="OVOS_CONFIG_USERNAME=ovos"
|
||||||
|
Environment="OVOS_CONFIG_PASSWORD=ovos"
|
||||||
|
WorkingDirectory=%h/.venvs/ovos
|
||||||
|
ExecStart=%h/.venvs/ovos/bin/ovos-skill-config-tool
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS Skills
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
After=ovos-messagebus.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
Group=ovos
|
||||||
|
UMask=002
|
||||||
|
ExecStart=%h/.venvs/ovos/bin/python /usr/libexec/ovos-systemd-skills
|
||||||
|
TimeoutStartSec=10m
|
||||||
|
TimeoutStopSec=1m
|
||||||
|
Restart=on-failure
|
||||||
|
StartLimitInterval=5min
|
||||||
|
StartLimitBurst=4
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS Spotifyd OCP Hooks
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
After=ovos-messagebus.service
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Group=ovos
|
||||||
|
UMask=002
|
||||||
|
ExecStart=/usr/bin/spotifyd --name raspOVOS --device-type "speaker" --initial-volume 100 --on-song-change-hook "/home/ovos/.venvs/ovos/bin/python /usr/libexec/ovos-spotifyd"
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS Config WebUI
|
||||||
|
PartOf=ovos.service
|
||||||
|
After=ovos.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Group=ovos
|
||||||
|
UMask=002
|
||||||
|
Environment="EDITOR_USERNAME=ovos"
|
||||||
|
Environment="EDITOR_PASSWORD=ovos"
|
||||||
|
WorkingDirectory=%h/.venvs/ovos
|
||||||
|
ExecStart=%h/.venvs/ovos/bin/ovos-yaml-editor
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=ovos.service
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=OVOS A.I. Software stack.
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
Group=ovos
|
||||||
|
ExecStart=/bin/true
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
{% set vault_addr = 'https://vault.trez.wtf' %}
|
||||||
|
{% set secrets_path = 'rikku-docker/env' %}
|
||||||
|
|
||||||
|
http:
|
||||||
|
pprof:
|
||||||
|
port: 6060
|
||||||
|
enabled: false
|
||||||
|
address: 0.0.0.0:80
|
||||||
|
session_ttl: 720h
|
||||||
|
users:
|
||||||
|
- name: admin
|
||||||
|
password: {{ lookup('community.hashi_vault.vault_kv2_get', 'env', engine_mount_point='rikku-docker', url=vault_addr, token=vault_token_cleaned)['secret']['ADGUARD_BCRYPT'] }}
|
||||||
|
auth_attempts: 5
|
||||||
|
block_auth_min: 15
|
||||||
|
http_proxy: ""
|
||||||
|
language: ""
|
||||||
|
theme: auto
|
||||||
|
dns:
|
||||||
|
bind_hosts:
|
||||||
|
- 0.0.0.0
|
||||||
|
port: 53
|
||||||
|
anonymize_client_ip: false
|
||||||
|
ratelimit: 20
|
||||||
|
ratelimit_subnet_len_ipv4: 24
|
||||||
|
ratelimit_subnet_len_ipv6: 56
|
||||||
|
ratelimit_whitelist: []
|
||||||
|
refuse_any: true
|
||||||
|
upstream_dns:
|
||||||
|
- 192.168.1.254
|
||||||
|
upstream_dns_file: ""
|
||||||
|
bootstrap_dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
fallback_dns: []
|
||||||
|
upstream_mode: load_balance
|
||||||
|
fastest_timeout: 1s
|
||||||
|
allowed_clients: []
|
||||||
|
disallowed_clients: []
|
||||||
|
blocked_hosts:
|
||||||
|
- version.bind
|
||||||
|
- id.server
|
||||||
|
- hostname.bind
|
||||||
|
trusted_proxies:
|
||||||
|
- 127.0.0.0/8
|
||||||
|
- ::1/128
|
||||||
|
cache_enabled: true
|
||||||
|
cache_size: 4194304
|
||||||
|
cache_ttl_min: 0
|
||||||
|
cache_ttl_max: 0
|
||||||
|
cache_optimistic: false
|
||||||
|
bogus_nxdomain: []
|
||||||
|
aaaa_disabled: false
|
||||||
|
enable_dnssec: false
|
||||||
|
edns_client_subnet:
|
||||||
|
custom_ip: ""
|
||||||
|
enabled: false
|
||||||
|
use_custom: false
|
||||||
|
max_goroutines: 300
|
||||||
|
handle_ddr: true
|
||||||
|
ipset: []
|
||||||
|
ipset_file: ""
|
||||||
|
bootstrap_prefer_ipv6: false
|
||||||
|
upstream_timeout: 10s
|
||||||
|
private_networks: []
|
||||||
|
use_private_ptr_resolvers: true
|
||||||
|
local_ptr_upstreams: []
|
||||||
|
use_dns64: false
|
||||||
|
dns64_prefixes: []
|
||||||
|
serve_http3: false
|
||||||
|
use_http3_upstreams: false
|
||||||
|
serve_plain_dns: true
|
||||||
|
hostsfile_enabled: true
|
||||||
|
pending_requests:
|
||||||
|
enabled: true
|
||||||
|
tls:
|
||||||
|
enabled: false
|
||||||
|
server_name: ""
|
||||||
|
force_https: false
|
||||||
|
port_https: 443
|
||||||
|
port_dns_over_tls: 853
|
||||||
|
port_dns_over_quic: 853
|
||||||
|
port_dnscrypt: 0
|
||||||
|
dnscrypt_config_file: ""
|
||||||
|
allow_unencrypted_doh: false
|
||||||
|
certificate_chain: ""
|
||||||
|
private_key: ""
|
||||||
|
certificate_path: ""
|
||||||
|
private_key_path: ""
|
||||||
|
strict_sni_check: false
|
||||||
|
querylog:
|
||||||
|
dir_path: ""
|
||||||
|
ignored: []
|
||||||
|
interval: 2160h
|
||||||
|
size_memory: 1000
|
||||||
|
enabled: true
|
||||||
|
file_enabled: true
|
||||||
|
statistics:
|
||||||
|
dir_path: ""
|
||||||
|
ignored: []
|
||||||
|
interval: 24h
|
||||||
|
enabled: true
|
||||||
|
filters:
|
||||||
|
- enabled: true
|
||||||
|
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
|
||||||
|
name: AdGuard DNS filter
|
||||||
|
id: 1
|
||||||
|
- enabled: false
|
||||||
|
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt
|
||||||
|
name: AdAway Default Blocklist
|
||||||
|
id: 2
|
||||||
|
whitelist_filters: []
|
||||||
|
user_rules: []
|
||||||
|
dhcp:
|
||||||
|
enabled: true
|
||||||
|
interface_name: wlan0
|
||||||
|
local_domain_name: lan
|
||||||
|
dhcpv4:
|
||||||
|
gateway_ip: 192.168.1.1
|
||||||
|
subnet_mask: 255.255.255.0
|
||||||
|
range_start: 192.168.1.2
|
||||||
|
range_end: 192.168.1.254
|
||||||
|
lease_duration: 86400
|
||||||
|
icmp_timeout_msec: 1000
|
||||||
|
options: []
|
||||||
|
dhcpv6:
|
||||||
|
range_start: ""
|
||||||
|
lease_duration: 86400
|
||||||
|
ra_slaac_only: false
|
||||||
|
ra_allow_slaac: false
|
||||||
|
filtering:
|
||||||
|
blocking_ipv4: ""
|
||||||
|
blocking_ipv6: ""
|
||||||
|
blocked_services:
|
||||||
|
schedule:
|
||||||
|
time_zone: America/New_York
|
||||||
|
ids: []
|
||||||
|
protection_disabled_until: null
|
||||||
|
safe_search:
|
||||||
|
enabled: false
|
||||||
|
bing: true
|
||||||
|
duckduckgo: true
|
||||||
|
ecosia: true
|
||||||
|
google: true
|
||||||
|
pixabay: true
|
||||||
|
yandex: true
|
||||||
|
youtube: true
|
||||||
|
blocking_mode: default
|
||||||
|
parental_block_host: family-block.dns.adguard.com
|
||||||
|
safebrowsing_block_host: standard-block.dns.adguard.com
|
||||||
|
rewrites: []
|
||||||
|
safe_fs_patterns:
|
||||||
|
- /opt/adguardhome/work/userfilters/*
|
||||||
|
safebrowsing_cache_size: 1048576
|
||||||
|
safesearch_cache_size: 1048576
|
||||||
|
parental_cache_size: 1048576
|
||||||
|
cache_time: 30
|
||||||
|
filters_update_interval: 24
|
||||||
|
blocked_response_ttl: 10
|
||||||
|
filtering_enabled: true
|
||||||
|
parental_enabled: false
|
||||||
|
safebrowsing_enabled: false
|
||||||
|
protection_enabled: true
|
||||||
|
clients:
|
||||||
|
runtime_sources:
|
||||||
|
whois: true
|
||||||
|
arp: true
|
||||||
|
rdns: true
|
||||||
|
dhcp: true
|
||||||
|
hosts: true
|
||||||
|
persistent: []
|
||||||
|
log:
|
||||||
|
enabled: true
|
||||||
|
file: ""
|
||||||
|
max_backups: 0
|
||||||
|
max_size: 100
|
||||||
|
max_age: 3
|
||||||
|
compress: false
|
||||||
|
local_time: false
|
||||||
|
verbose: false
|
||||||
|
os:
|
||||||
|
group: ""
|
||||||
|
user: ""
|
||||||
|
rlimit_nofile: 0
|
||||||
|
schema_version: 30
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
- id: '1654749081487'
|
||||||
|
alias: Vacuum schedule
|
||||||
|
description: ''
|
||||||
|
trigger:
|
||||||
|
- platform: time
|
||||||
|
at: '11:00:00'
|
||||||
|
condition: []
|
||||||
|
action:
|
||||||
|
- parallel:
|
||||||
|
- device_id: 4f4ac1838a8d32748da9d2e5aa54c748
|
||||||
|
domain: vacuum
|
||||||
|
entity_id: vacuum.johnny_5
|
||||||
|
type: clean
|
||||||
|
- service: vacuum.set_fan_speed
|
||||||
|
data:
|
||||||
|
fan_speed: Eco
|
||||||
|
target:
|
||||||
|
device_id: 4f4ac1838a8d32748da9d2e5aa54c748
|
||||||
|
entity_id: vacuum.johnny_5
|
||||||
|
mode: single
|
||||||
|
- id: '1687209973222'
|
||||||
|
alias: After Pool
|
||||||
|
description: Night lights
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id:
|
||||||
|
- binary_sensor.front_door_sensor_opening
|
||||||
|
from: 'off'
|
||||||
|
to: 'on'
|
||||||
|
condition:
|
||||||
|
- condition: and
|
||||||
|
conditions:
|
||||||
|
- condition: device
|
||||||
|
device_id: 8381d589cc257ba882fb80036224e53d
|
||||||
|
domain: device_tracker
|
||||||
|
entity_id: device_tracker.pixel_7_3
|
||||||
|
type: is_home
|
||||||
|
- condition: sun
|
||||||
|
before: sunrise
|
||||||
|
after: sunset
|
||||||
|
- condition: time
|
||||||
|
weekday:
|
||||||
|
- mon
|
||||||
|
- tue
|
||||||
|
- thu
|
||||||
|
action:
|
||||||
|
- service: scene.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: scene.night_time
|
||||||
|
metadata: {}
|
||||||
|
mode: single
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
homeassistant:
|
||||||
|
# Name of the location where Home Assistant is running
|
||||||
|
name: "Rikku"
|
||||||
|
# Location required to calculate the time the sun rises and sets
|
||||||
|
latitude: 40
|
||||||
|
longitude: -73
|
||||||
|
# 'metric' for Metric, 'us_customary' for US Customary
|
||||||
|
unit_system: us_customary
|
||||||
|
# Pick yours from here: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||||
|
time_zone: "America/New_York"
|
||||||
|
|
||||||
|
# Configure a default setup of Home Assistant (frontend, api, etc)
|
||||||
|
default_config:
|
||||||
|
|
||||||
|
# Text to speech
|
||||||
|
tts:
|
||||||
|
- platform: google_translate
|
||||||
|
|
||||||
|
# automation: !include automations.yaml
|
||||||
|
# script: !include scripts.yaml
|
||||||
|
# scene: !include scenes.yaml
|
||||||
|
|
||||||
|
http:
|
||||||
|
use_x_forwarded_for: true
|
||||||
|
trusted_proxies: 192.168.1.254
|
||||||
|
|
||||||
|
browser:
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
|
"""Set up the Cloudflare Tunnel component."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Set up Cloudflare Tunnel from a config entry."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = entry.data
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, ["sensor"])
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "sensor")
|
||||||
|
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Failed to unload entry: %s", entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
import aiohttp
|
||||||
|
import async_timeout
|
||||||
|
import voluptuous as vol
|
||||||
|
from homeassistant import config_entries, exceptions
|
||||||
|
from .const import DOMAIN, CONF_API_KEY, CONF_ACCOUNT_ID, LABEL_API_KEY, LABEL_ACCOUNT_ID, PLACEHOLDER_API_KEY, PLACEHOLDER_ACCOUNT_ID
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
URL = "https://api.cloudflare.com/client/v4/user/tokens/verify"
|
||||||
|
TIMEOUT = 10
|
||||||
|
|
||||||
|
# Custom exceptions
|
||||||
|
class CannotConnect(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
class InvalidAuth(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate there is invalid auth."""
|
||||||
|
|
||||||
|
DATA_SCHEMA = vol.Schema({
|
||||||
|
vol.Required(CONF_ACCOUNT_ID, description={LABEL_ACCOUNT_ID}): str,
|
||||||
|
vol.Required(CONF_API_KEY, description={LABEL_API_KEY}): str
|
||||||
|
})
|
||||||
|
|
||||||
|
async def validate_credentials(hass, data):
|
||||||
|
"""Validate the provided credentials are correct."""
|
||||||
|
api_key = data["api_key"]
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'Bearer {api_key}',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with async_timeout.timeout(TIMEOUT):
|
||||||
|
async with session.get(URL, headers=headers) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
return True
|
||||||
|
elif response.status == 401:
|
||||||
|
raise InvalidAuth
|
||||||
|
else:
|
||||||
|
raise CannotConnect
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
raise CannotConnect
|
||||||
|
except async_timeout.TimeoutError:
|
||||||
|
raise CannotConnect
|
||||||
|
|
||||||
|
class CloudflareConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Cloudflare config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initiated by the user."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
await validate_credentials(self.hass, user_input)
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception:
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
return self.async_create_entry(title="Cloudflare Tunnel Monitor", data=user_input)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=DATA_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
CONF_API_KEY: PLACEHOLDER_API_KEY,
|
||||||
|
CONF_ACCOUNT_ID: PLACEHOLDER_ACCOUNT_ID,
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
DOMAIN = "cloudflare_tunnel_monitor"
|
||||||
|
|
||||||
|
# Constants for user data keys
|
||||||
|
CONF_ACCOUNT_ID = "account_id"
|
||||||
|
CONF_API_KEY = "api_key"
|
||||||
|
|
||||||
|
|
||||||
|
# Constants for labels displayed in the user interface
|
||||||
|
LABEL_ACCOUNT_ID = "Account ID"
|
||||||
|
LABEL_API_KEY = "API Token"
|
||||||
|
|
||||||
|
# Constants for placeholders or suggested values
|
||||||
|
PLACEHOLDER_API_KEY = "api-token"
|
||||||
|
PLACEHOLDER_ACCOUNT_ID = "your-account-id"
|
||||||
+249
@@ -0,0 +1,249 @@
|
|||||||
|
import logging
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import async_timeout
|
||||||
|
from homeassistant.components.sensor import SensorEntity, SensorDeviceClass
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
|
from datetime import timedelta
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
URL = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel?is_deleted=false"
|
||||||
|
TIMEOUT = 10
|
||||||
|
RETRY_DELAY = 20
|
||||||
|
MAX_RETRIES = 5
|
||||||
|
|
||||||
|
def create_headers(api_key):
|
||||||
|
"""Create headers for API requests."""
|
||||||
|
return {
|
||||||
|
'Authorization': f'Bearer {api_key}',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fetch_tunnels(api_key, account_id, hass, entry_id, retries=0):
|
||||||
|
"""Retrieve Cloudflare tunnel status using aiohttp."""
|
||||||
|
headers = create_headers(api_key)
|
||||||
|
url = URL.format(account_id)
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Attempt {retries + 1} to fetch tunnels from URL: {url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with async_timeout.timeout(TIMEOUT):
|
||||||
|
async with session.get(url, headers=headers) as response:
|
||||||
|
_LOGGER.debug(f"Response status: {response.status}")
|
||||||
|
if response.status == 200:
|
||||||
|
json_response = await response.json()
|
||||||
|
_LOGGER.debug(f"Received data: {json_response}")
|
||||||
|
return json_response['result']
|
||||||
|
elif response.status == 401:
|
||||||
|
raise UpdateFailed("Unauthorized access - check your API key")
|
||||||
|
else:
|
||||||
|
raise UpdateFailed(f"Error fetching Cloudflare tunnels: {response.status}, {response.reason}")
|
||||||
|
except aiohttp.ClientError as err:
|
||||||
|
_LOGGER.error(f"Client error fetching data: {err}")
|
||||||
|
raise UpdateFailed("Client error occurred while fetching data") from err
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.error("Timeout error fetching data")
|
||||||
|
raise UpdateFailed("Timeout error occurred while fetching data")
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error(f"Unexpected error fetching data: {err}")
|
||||||
|
if retries < MAX_RETRIES:
|
||||||
|
_LOGGER.info(f"Retrying in {RETRY_DELAY} seconds...")
|
||||||
|
await asyncio.sleep(RETRY_DELAY)
|
||||||
|
return await fetch_tunnels(api_key, account_id, hass, entry_id, retries + 1)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Maximum number of retries reached, scheduling integration reload")
|
||||||
|
await schedule_integration_reload(hass, entry_id)
|
||||||
|
raise UpdateFailed("Maximum retries reached, integration reload scheduled")
|
||||||
|
|
||||||
|
class CloudflareTunnelsDevice(Entity):
|
||||||
|
"""Representation of the Cloudflare Tunnels device."""
|
||||||
|
|
||||||
|
def __init__(self, account_id, domain):
|
||||||
|
"""Initialize the Cloudflare Tunnels device."""
|
||||||
|
self._account_id = account_id
|
||||||
|
self._domain = domain
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return f"{self._domain}_cloudflare_tunnels_{self._account_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return "Cloudflare Tunnels"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device information."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(self._domain, self.unique_id)},
|
||||||
|
"name": self.name,
|
||||||
|
"manufacturer": "Cloudflare",
|
||||||
|
}
|
||||||
|
|
||||||
|
class CloudflareTunnelSensor(SensorEntity):
|
||||||
|
"""Representation of a Cloudflare tunnel sensor."""
|
||||||
|
|
||||||
|
def __init__(self, tunnel, coordinator, device):
|
||||||
|
"""Initialize the Cloudflare tunnel sensor."""
|
||||||
|
self.coordinator = coordinator
|
||||||
|
self._tunnel = tunnel
|
||||||
|
self._device = device
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return f"Cloudflare Tunnel {self._tunnel['name']}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return f"{self._device._domain}_{self._tunnel['id']}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self._tunnel['status']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Return the icon of the sensor."""
|
||||||
|
return 'mdi:cloud-check' if self._tunnel['status'] == 'healthy' else 'mdi:cloud-off-outline'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self):
|
||||||
|
"""Return the possible values of the sensor."""
|
||||||
|
return ["inactive", "degraded", "healthy", "down"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the device class of the sensor."""
|
||||||
|
return SensorDeviceClass.ENUM
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device information."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(self._device._domain, self._device.unique_id)},
|
||||||
|
"name": self._device.name,
|
||||||
|
"manufacturer": "Cloudflare",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update the state of the sensor."""
|
||||||
|
_LOGGER.debug(f"Requesting refresh for tunnel {self._tunnel['id']}")
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
if self.coordinator.data is not None:
|
||||||
|
_LOGGER.debug(f"Coordinator data is not None. Searching for updated tunnel data for {self._tunnel['id']}")
|
||||||
|
|
||||||
|
updated_tunnel = next((tunnel for tunnel in self.coordinator.data if tunnel.get('id') == self._tunnel.get('id')), None)
|
||||||
|
if updated_tunnel is not None:
|
||||||
|
_LOGGER.debug(f"Found updated data for tunnel {self._tunnel['id']}")
|
||||||
|
self._tunnel = updated_tunnel
|
||||||
|
_LOGGER.debug("Tunnel updated data: %s", self._tunnel)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Tunnel with ID %s not found in the updated data", self._tunnel.get('id'))
|
||||||
|
else:
|
||||||
|
_LOGGER.error("No data received in coordinator during update, maintaining previous state")
|
||||||
|
|
||||||
|
class CloudflareTunnelManager:
|
||||||
|
"""Manages Cloudflare Tunnel Sensor entities."""
|
||||||
|
|
||||||
|
def __init__(self, hass, async_add_entities, coordinator, device):
|
||||||
|
self._hass = hass
|
||||||
|
self._async_add_entities = async_add_entities
|
||||||
|
self._coordinator = coordinator
|
||||||
|
self._device = device
|
||||||
|
self._sensors = {}
|
||||||
|
|
||||||
|
async def update_sensors(self, new_tunnels, removed_tunnels):
|
||||||
|
"""Update sensor entities based on the tunnel changes."""
|
||||||
|
_LOGGER.debug(f"Updating sensors. New: {new_tunnels}, Removed: {removed_tunnels}")
|
||||||
|
|
||||||
|
for tunnel in new_tunnels:
|
||||||
|
sensor_id = f"{self._device._domain}_{tunnel['id']}"
|
||||||
|
if sensor_id not in self._sensors:
|
||||||
|
_LOGGER.info(f"Adding new sensor for tunnel: {tunnel['id']}")
|
||||||
|
sensor = CloudflareTunnelSensor(tunnel, self._coordinator, self._device)
|
||||||
|
self._sensors[sensor_id] = sensor
|
||||||
|
self._async_add_entities([sensor], True)
|
||||||
|
|
||||||
|
for tunnel in removed_tunnels:
|
||||||
|
sensor_id = f"{self._device._domain}_{tunnel['id']}"
|
||||||
|
if sensor_id in self._sensors:
|
||||||
|
_LOGGER.info(f"Removing sensor for tunnel: {sensor_id}")
|
||||||
|
try:
|
||||||
|
sensor = self._sensors.pop(sensor_id)
|
||||||
|
await sensor.async_remove()
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Error removing sensor for tunnel {sensor_id}: {e}")
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up the Cloudflare tunnel sensor."""
|
||||||
|
api_key = config_entry.data["api_key"]
|
||||||
|
account_id = config_entry.data["account_id"]
|
||||||
|
device = CloudflareTunnelsDevice(account_id, DOMAIN)
|
||||||
|
|
||||||
|
async def async_update_data():
|
||||||
|
"""Fetch data from API endpoint and detect changes in tunnels."""
|
||||||
|
_LOGGER.debug("Fetching new tunnel data from Cloudflare")
|
||||||
|
new_data = await fetch_tunnels(api_key, account_id, hass, config_entry.entry_id)
|
||||||
|
if new_data is None:
|
||||||
|
new_data = []
|
||||||
|
|
||||||
|
if coordinator.data is None:
|
||||||
|
coordinator.data = []
|
||||||
|
|
||||||
|
current_ids = {tunnel['id'] for tunnel in coordinator.data}
|
||||||
|
new_ids = {tunnel['id'] for tunnel in new_data}
|
||||||
|
|
||||||
|
added_tunnels = [tunnel for tunnel in new_data if tunnel['id'] not in current_ids]
|
||||||
|
|
||||||
|
removed_tunnels = [tunnel for tunnel in coordinator.data if tunnel['id'] not in new_ids]
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Added tunnels: {added_tunnels}, Removed tunnels: {removed_tunnels}")
|
||||||
|
|
||||||
|
if added_tunnels or removed_tunnels:
|
||||||
|
await tunnel_manager.update_sensors(added_tunnels, removed_tunnels)
|
||||||
|
|
||||||
|
return new_data
|
||||||
|
|
||||||
|
coordinator = DataUpdateCoordinator(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name="cloudflare_tunnel",
|
||||||
|
update_method=async_update_data,
|
||||||
|
update_interval=timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
tunnel_manager = CloudflareTunnelManager(hass, async_add_entities, coordinator, device)
|
||||||
|
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
if not hasattr(tunnel_manager, 'initialized') or not tunnel_manager.initialized:
|
||||||
|
_LOGGER.debug("Creating initial sensor entities")
|
||||||
|
for tunnel in coordinator.data:
|
||||||
|
sensor_id = f"{device._domain}_{tunnel['id']}"
|
||||||
|
if sensor_id not in tunnel_manager._sensors:
|
||||||
|
sensor = CloudflareTunnelSensor(tunnel, coordinator, device)
|
||||||
|
tunnel_manager._sensors[sensor_id] = sensor
|
||||||
|
async_add_entities([sensor], True)
|
||||||
|
tunnel_manager.initialized = True
|
||||||
|
|
||||||
|
hass.bus.async_listen_once("homeassistant_stop", async_shutdown)
|
||||||
|
|
||||||
|
async def async_shutdown(event):
|
||||||
|
"""Close aiohttp session when Home Assistant stops."""
|
||||||
|
_LOGGER.debug("Cloudflare Tunnel Monitor - aiohttp session closed")
|
||||||
|
|
||||||
|
async def schedule_integration_reload(hass, entry_id):
|
||||||
|
"""Schedule a reload of the integration."""
|
||||||
|
_LOGGER.info(f"Scheduling reload of integration with entry_id {entry_id}")
|
||||||
|
await hass.config_entries.async_reload(entry_id)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""The Cync Room Lights integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .cync_hub import CyncHub
|
||||||
|
|
||||||
|
PLATFORMS: list[str] = ["light","binary_sensor","switch","fan"]
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Cync Room Lights from a config entry."""
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
remove_options_update_listener = entry.add_update_listener(options_update_listener)
|
||||||
|
hub = CyncHub(entry.data, entry.options, remove_options_update_listener)
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = hub
|
||||||
|
hub.start_tcp_client()
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def options_update_listener(
|
||||||
|
hass: HomeAssistant, config_entry: config_entries.ConfigEntry
|
||||||
|
):
|
||||||
|
"""Handle options update."""
|
||||||
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
hub = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
hub.remove_options_update_listener()
|
||||||
|
hub.disconnect()
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
"""Platform for binary sensor integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
from homeassistant.components.binary_sensor import (BinarySensorDeviceClass, BinarySensorEntity)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
new_devices = []
|
||||||
|
for sensor in hub.cync_motion_sensors:
|
||||||
|
if not hub.cync_motion_sensors[sensor]._update_callback and sensor in config_entry.options["motion_sensors"]:
|
||||||
|
new_devices.append(CyncMotionSensorEntity(hub.cync_motion_sensors[sensor]))
|
||||||
|
for sensor in hub.cync_ambient_light_sensors:
|
||||||
|
if not hub.cync_ambient_light_sensors[sensor]._update_callback and sensor in config_entry.options["ambient_light_sensors"]:
|
||||||
|
new_devices.append(CyncAmbientLightSensorEntity(hub.cync_ambient_light_sensors[sensor]))
|
||||||
|
|
||||||
|
if new_devices:
|
||||||
|
async_add_entities(new_devices)
|
||||||
|
|
||||||
|
|
||||||
|
class CyncMotionSensorEntity(BinarySensorEntity):
|
||||||
|
"""Representation of a Cync Motion Sensor."""
|
||||||
|
|
||||||
|
should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, motion_sensor) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.motion_sensor = motion_sensor
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when this Entity has been added to HA."""
|
||||||
|
self.motion_sensor.register(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Entity being removed from hass."""
|
||||||
|
self.motion_sensor.reset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device registry information for this entity."""
|
||||||
|
return DeviceInfo(
|
||||||
|
identifiers = {(DOMAIN, f"{self.motion_sensor.room.name} ({self.motion_sensor.home_name})")},
|
||||||
|
manufacturer = "Cync by Savant",
|
||||||
|
name = f"{self.motion_sensor.room.name} ({self.motion_sensor.home_name})",
|
||||||
|
suggested_area = f"{self.motion_sensor.room.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return Unique ID string."""
|
||||||
|
return 'cync_motion_sensor_' + self.motion_sensor.device_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the motion_sensor."""
|
||||||
|
return self.motion_sensor.name + " Motion"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if light is on."""
|
||||||
|
return self.motion_sensor.motion
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> str | None:
|
||||||
|
"""Return the device class"""
|
||||||
|
return BinarySensorDeviceClass.MOTION
|
||||||
|
|
||||||
|
class CyncAmbientLightSensorEntity(BinarySensorEntity):
|
||||||
|
"""Representation of a Cync Ambient Light Sensor."""
|
||||||
|
|
||||||
|
should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, ambient_light_sensor) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.ambient_light_sensor = ambient_light_sensor
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when this Entity has been added to HA."""
|
||||||
|
self.ambient_light_sensor.register(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Entity being removed from hass."""
|
||||||
|
self.ambient_light_sensor.reset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device registry information for this entity."""
|
||||||
|
return DeviceInfo(
|
||||||
|
identifiers = {(DOMAIN, f"{self.ambient_light_sensor.room.name} ({self.ambient_light_sensor.home_name})")},
|
||||||
|
manufacturer = "Cync by Savant",
|
||||||
|
name = f"{self.ambient_light_sensor.room.name} ({self.ambient_light_sensor.home_name})",
|
||||||
|
suggested_area = f"{self.ambient_light_sensor.room.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return Unique ID string."""
|
||||||
|
return 'cync_ambient_light_sensor_' + self.ambient_light_sensor.device_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the ambient_light_sensor."""
|
||||||
|
return self.ambient_light_sensor.name + " Ambient Light"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if light is on."""
|
||||||
|
return self.ambient_light_sensor.ambient_light
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> str | None:
|
||||||
|
"""Return the device class"""
|
||||||
|
return BinarySensorDeviceClass.LIGHT
|
||||||
|
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
"""Config flow for Cync Room Lights integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
from typing import Any
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .cync_hub import CyncUserData
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("username"): str,
|
||||||
|
vol.Required("password"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
STEP_TWO_FACTOR_CODE = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("two_factor_code"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def cync_login(hub, user_input: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Validate the user input"""
|
||||||
|
|
||||||
|
response = await hub.authenticate(user_input["username"], user_input["password"])
|
||||||
|
if response['authorized']:
|
||||||
|
return {'title':'cync_lights_'+ user_input['username'],'data':{'cync_credentials': hub.auth_code, 'user_input':user_input}}
|
||||||
|
else:
|
||||||
|
if response['two_factor_code_required']:
|
||||||
|
raise TwoFactorCodeRequired
|
||||||
|
else:
|
||||||
|
raise InvalidAuth
|
||||||
|
|
||||||
|
async def submit_two_factor_code(hub, user_input: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Validate the two factor code"""
|
||||||
|
|
||||||
|
response = await hub.auth_two_factor(user_input["two_factor_code"])
|
||||||
|
if response['authorized']:
|
||||||
|
return {'title':'cync_lights_'+ hub.username,'data':{'cync_credentials': hub.auth_code, 'user_input': {'username':hub.username,'password':hub.password}}}
|
||||||
|
else:
|
||||||
|
raise InvalidAuth
|
||||||
|
|
||||||
|
class CyncConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Cync Room Lights."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.cync_hub = CyncUserData()
|
||||||
|
self.data ={}
|
||||||
|
self.options = {}
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle user and password for Cync account."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||||
|
)
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = await cync_login(self.cync_hub, user_input)
|
||||||
|
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
|
||||||
|
except TwoFactorCodeRequired:
|
||||||
|
return await self.async_step_two_factor_code()
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
_LOGGER.error(str(type(e).__name__) + ": " + str(e))
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
self.data = info
|
||||||
|
return await self.async_step_finish_setup()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_two_factor_code(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle two factor authentication for Cync account."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="two_factor_code", data_schema=STEP_TWO_FACTOR_CODE
|
||||||
|
)
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = await submit_two_factor_code(self.cync_hub, user_input)
|
||||||
|
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
_LOGGER.error(str(type(e).__name__) + ": " + str(e))
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
self.data = info
|
||||||
|
return await self.async_step_select_switches()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_step_select_switches(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Select rooms and individual switches for entity creation"""
|
||||||
|
if user_input is not None:
|
||||||
|
self.options = user_input
|
||||||
|
return await self._async_finish_setup()
|
||||||
|
|
||||||
|
switches_data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(
|
||||||
|
"rooms",
|
||||||
|
description = {"suggested_value" : [room for room in self.data["data"]["cync_config"]["rooms"].keys() if not self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']]},
|
||||||
|
): cv.multi_select({room : f'{room_info["name"]} ({room_info["home_name"]})' for room,room_info in self.data["data"]["cync_config"]["rooms"].items() if not self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
|
||||||
|
vol.Optional(
|
||||||
|
"subgroups",
|
||||||
|
description = {"suggested_value" : [room for room in self.data["data"]["cync_config"]["rooms"].keys() if self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']]},
|
||||||
|
): cv.multi_select({room : f'{room_info["name"]} ({room_info.get("parent_room","")}:{room_info["home_name"]})' for room,room_info in self.data["data"]["cync_config"]["rooms"].items() if self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
|
||||||
|
vol.Optional(
|
||||||
|
"switches",
|
||||||
|
description = {"suggested_value" : [device_id for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info['FAN']]},
|
||||||
|
): cv.multi_select({switch_id : f'{sw_info["name"]} ({sw_info["room_name"]}:{sw_info["home_name"]})' for switch_id,sw_info in self.data["data"]["cync_config"]["devices"].items() if sw_info.get('ONOFF',False) and sw_info.get('MULTIELEMENT',1) == 1}),
|
||||||
|
vol.Optional(
|
||||||
|
"motion_sensors",
|
||||||
|
description = {"suggested_value" : [device_id for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info['MOTION']]},
|
||||||
|
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info.get('MOTION',False)}),
|
||||||
|
vol.Optional(
|
||||||
|
"ambient_light_sensors",
|
||||||
|
description = {"suggested_value" : [device_id for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info['AMBIENT_LIGHT']]},
|
||||||
|
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info.get('AMBIENT_LIGHT',False)}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="select_switches", data_schema=switches_data_schema)
|
||||||
|
|
||||||
|
async def _async_finish_setup(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Finish setup and create entry"""
|
||||||
|
|
||||||
|
existing_entry = await self.async_set_unique_id(self.data['title'])
|
||||||
|
if not existing_entry:
|
||||||
|
return self.async_create_entry(title=self.data["title"], data=self.data["data"], options=self.options)
|
||||||
|
else:
|
||||||
|
self.hass.config_entries.async_update_entry(existing_entry, data=self.data['data'], options=self.options)
|
||||||
|
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||||
|
return self.hass.config_entries.async_abort(reason="reauth_successful")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return CyncOptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
|
class CyncOptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.entry = config_entry
|
||||||
|
self.cync_hub = CyncUserData()
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Manage the options."""
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
if user_input['re-authenticate'] == "No":
|
||||||
|
return await self.async_step_select_switches()
|
||||||
|
else:
|
||||||
|
return await self.async_step_auth()
|
||||||
|
|
||||||
|
data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
"re-authenticate",default="No"): vol.In(["Yes","No"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||||
|
|
||||||
|
async def async_step_auth(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Attempt to re-authenticate"""
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = await cync_login(self.cync_hub, self.entry.data['user_input'])
|
||||||
|
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
|
||||||
|
except TwoFactorCodeRequired:
|
||||||
|
return await self.async_step_two_factor_code()
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
_LOGGER.error(str(type(e).__name__) + ": " + str(e))
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
self.data = info
|
||||||
|
return await self.async_step_select_switches()
|
||||||
|
|
||||||
|
async def async_step_two_factor_code(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle two factor authentication for Cync account."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="two_factor_code", data_schema=STEP_TWO_FACTOR_CODE
|
||||||
|
)
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
info = await submit_two_factor_code(self.cync_hub, user_input)
|
||||||
|
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
_LOGGER.error(str(type(e).__name__) + ": " + str(e))
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
self.data = info
|
||||||
|
return await self.async_step_select_switches()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="two_factor_code", data_schema=STEP_TWO_FACTOR_CODE, errors=errors
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_select_switches(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Manage the options."""
|
||||||
|
|
||||||
|
if "data" in self.data and self.data["data"] != self.entry.data:
|
||||||
|
self.hass.config_entries.async_update_entry(self.entry, data = self.data["data"])
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="",data=user_input)
|
||||||
|
|
||||||
|
switches_data_schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(
|
||||||
|
"rooms",
|
||||||
|
description = {"suggested_value" : [room for room in self.entry.options["rooms"] if room in self.entry.data["cync_config"]["rooms"].keys()]},
|
||||||
|
): cv.multi_select({room : f'{room_info["name"]} ({room_info["home_name"]})' for room,room_info in self.entry.data["cync_config"]["rooms"].items() if not self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
|
||||||
|
vol.Optional(
|
||||||
|
"subgroups",
|
||||||
|
description = {"suggested_value" : [room for room in self.entry.options["subgroups"] if room in self.entry.data["cync_config"]["rooms"].keys()]},
|
||||||
|
): cv.multi_select({room : f'{room_info["name"]} ({room_info.get("parent_room","")}:{room_info["home_name"]})' for room,room_info in self.entry.data["cync_config"]["rooms"].items() if self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
|
||||||
|
vol.Optional(
|
||||||
|
"switches",
|
||||||
|
description = {"suggested_value" : [sw for sw in self.entry.options["switches"] if sw in self.entry.data["cync_config"]["devices"].keys()]},
|
||||||
|
): cv.multi_select({switch_id : f'{sw_info["name"]} ({sw_info["room_name"]}:{sw_info["home_name"]})' for switch_id,sw_info in self.entry.data["cync_config"]["devices"].items() if sw_info.get('ONOFF',False) and sw_info.get('MULTIELEMENT',1) == 1}),
|
||||||
|
vol.Optional(
|
||||||
|
"motion_sensors",
|
||||||
|
description = {"suggested_value" : [sensor for sensor in self.entry.options["motion_sensors"] if sensor in self.entry.data["cync_config"]["devices"].keys()]},
|
||||||
|
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.entry.data["cync_config"]["devices"].items() if device_info.get('MOTION',False)}),
|
||||||
|
vol.Optional(
|
||||||
|
"ambient_light_sensors",
|
||||||
|
description = {"suggested_value" : [sensor for sensor in self.entry.options["ambient_light_sensors"] if sensor in self.entry.data["cync_config"]["devices"].keys()]},
|
||||||
|
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.entry.data["cync_config"]["devices"].items() if device_info.get('AMBIENT_LIGHT',False)}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="select_switches", data_schema=switches_data_schema)
|
||||||
|
|
||||||
|
class TwoFactorCodeRequired(HomeAssistantError):
|
||||||
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
class InvalidAuth(HomeAssistantError):
|
||||||
|
"""Error to indicate there is invalid auth."""
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""Constants for the Cync Room Lights integration."""
|
||||||
|
|
||||||
|
DOMAIN = "cync_lights"
|
||||||
@@ -0,0 +1,870 @@
|
|||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import asyncio
|
||||||
|
import struct
|
||||||
|
import aiohttp
|
||||||
|
import math
|
||||||
|
import ssl
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
API_AUTH = "https://api.gelighting.com/v2/user_auth"
|
||||||
|
API_REQUEST_CODE = "https://api.gelighting.com/v2/two_factor/email/verifycode"
|
||||||
|
API_2FACTOR_AUTH = "https://api.gelighting.com/v2/user_auth/two_factor"
|
||||||
|
API_DEVICES = "https://api.gelighting.com/v2/user/{user}/subscribe/devices"
|
||||||
|
API_DEVICE_INFO = "https://api.gelighting.com/v2/product/{product_id}/device/{device_id}/property"
|
||||||
|
|
||||||
|
Capabilities = {
|
||||||
|
"ONOFF":[1,5,6,7,8,9,10,11,13,14,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,48,49,51,52,53,54,55,56,57,58,59,61,62,63,64,65,66,67,68,80,81,82,83,85,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170],
|
||||||
|
"BRIGHTNESS":[1,5,6,7,8,9,10,11,13,14,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,48,49,55,56,80,81,82,83,85,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170],
|
||||||
|
"COLORTEMP":[5,6,7,8,10,11,14,15,19,20,21,22,23,25,26,28,29,30,31,32,33,34,35,80,82,83,85,129,130,131,132,133,135,136,137,138,139,140,141,142,143,144,145,146,147,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170],
|
||||||
|
"RGB":[6,7,8,21,22,23,30,31,32,33,34,35,131,132,133,137,138,139,140,141,142,143,146,147,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170],
|
||||||
|
"MOTION":[37,49,54],
|
||||||
|
"AMBIENT_LIGHT":[37,49,54],
|
||||||
|
"WIFICONTROL":[36,37,38,39,40,48,49,51,52,53,54,55,56,57,58,59,61,62,63,64,65,66,67,68,80,81,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170],
|
||||||
|
"PLUG":[64,65,66,67,68],
|
||||||
|
"FAN":[81],
|
||||||
|
"MULTIELEMENT":{'67':2}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CyncHub:
|
||||||
|
|
||||||
|
def __init__(self, user_data, options, remove_options_update_listener):
|
||||||
|
|
||||||
|
self.thread = None
|
||||||
|
self.loop = None
|
||||||
|
self.reader = None
|
||||||
|
self.writer = None
|
||||||
|
self.login_code = bytearray(user_data['cync_credentials'])
|
||||||
|
self.logged_in = False
|
||||||
|
self.home_devices = user_data['cync_config']['home_devices']
|
||||||
|
self.home_controllers = user_data['cync_config']['home_controllers']
|
||||||
|
self.switchID_to_homeID = user_data['cync_config']['switchID_to_homeID']
|
||||||
|
self.connected_devices = {home_id:[] for home_id in self.home_controllers.keys()}
|
||||||
|
self.shutting_down = False
|
||||||
|
self.remove_options_update_listener = remove_options_update_listener
|
||||||
|
self.cync_rooms = {room_id:CyncRoom(room_id,room_info,self) for room_id,room_info in user_data['cync_config']['rooms'].items()}
|
||||||
|
self.cync_switches = {device_id:CyncSwitch(device_id,switch_info,self.cync_rooms.get(switch_info['room'], None),self) for device_id,switch_info in user_data['cync_config']['devices'].items() if switch_info.get("ONOFF",False)}
|
||||||
|
self.cync_motion_sensors = {device_id:CyncMotionSensor(device_id,device_info,self.cync_rooms.get(device_info['room'], None)) for device_id,device_info in user_data['cync_config']['devices'].items() if device_info.get("MOTION",False)}
|
||||||
|
self.cync_ambient_light_sensors = {device_id:CyncAmbientLightSensor(device_id,device_info,self.cync_rooms.get(device_info['room'], None)) for device_id,device_info in user_data['cync_config']['devices'].items() if device_info.get("AMBIENT_LIGHT",False)}
|
||||||
|
self.switchID_to_deviceIDs = {device_info.switch_id:[dev_id for dev_id, dev_info in self.cync_switches.items() if dev_info.switch_id == device_info.switch_id] for device_id, device_info in self.cync_switches.items() if int(device_info.switch_id) > 0}
|
||||||
|
self.connected_devices_updated = False
|
||||||
|
self.options = options
|
||||||
|
self._seq_num = 0
|
||||||
|
self.pending_commands = {}
|
||||||
|
[room.initialize() for room in self.cync_rooms.values() if room.is_subgroup]
|
||||||
|
[room.initialize() for room in self.cync_rooms.values() if not room.is_subgroup]
|
||||||
|
|
||||||
|
def start_tcp_client(self):
|
||||||
|
self.thread = threading.Thread(target=self._start_tcp_client,daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def _start_tcp_client(self):
|
||||||
|
self.loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(self.loop)
|
||||||
|
self.loop.run_until_complete(self._connect())
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
self.shutting_down = True
|
||||||
|
for home_controllers in self.home_controllers.values(): #send packets to server to generate data to be read which will initiate shutdown
|
||||||
|
for controller in home_controllers:
|
||||||
|
seq = self.get_seq_num()
|
||||||
|
state_request = bytes.fromhex('7300000018') + int(controller).to_bytes(4,'big') + seq.to_bytes(2,'big') + bytes.fromhex('007e00000000f85206000000ffff0000567e')
|
||||||
|
self.loop.call_soon_threadsafe(self.send_request,state_request)
|
||||||
|
|
||||||
|
async def _connect(self):
|
||||||
|
while not self.shutting_down:
|
||||||
|
try:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
try:
|
||||||
|
self.reader, self.writer = await asyncio.open_connection('cm.gelighting.com', 23779, ssl = context)
|
||||||
|
except Exception as e:
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
try:
|
||||||
|
self.reader, self.writer = await asyncio.open_connection('cm.gelighting.com', 23779, ssl = context)
|
||||||
|
except Exception as e:
|
||||||
|
self.reader, self.writer = await asyncio.open_connection('cm.gelighting.com', 23778)
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(str(type(e).__name__) + ": " + str(e))
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
else:
|
||||||
|
read_tcp_messages = asyncio.create_task(self._read_tcp_messages(), name = "Read TCP Messages")
|
||||||
|
maintain_connection = asyncio.create_task(self._maintain_connection(), name = "Maintain Connection")
|
||||||
|
update_state = asyncio.create_task(self._update_state(), name = "Update State")
|
||||||
|
update_connected_devices = asyncio.create_task(self._update_connected_devices(), name = "Update Connected Devices")
|
||||||
|
read_write_tasks = [read_tcp_messages, maintain_connection, update_state, update_connected_devices]
|
||||||
|
try:
|
||||||
|
done, pending = await asyncio.wait(read_write_tasks,return_when=asyncio.FIRST_EXCEPTION)
|
||||||
|
for task in done:
|
||||||
|
name = task.get_name()
|
||||||
|
exception = task.exception()
|
||||||
|
try:
|
||||||
|
result = task.result()
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(str(type(e).__name__) + ": " + str(e))
|
||||||
|
for task in pending:
|
||||||
|
task.cancel()
|
||||||
|
if not self.shutting_down:
|
||||||
|
_LOGGER.error("Connection to Cync server reset, restarting in 15 seconds")
|
||||||
|
await asyncio.sleep(15)
|
||||||
|
else:
|
||||||
|
_LOGGER.debug("Cync client shutting down")
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(str(type(e).__name__) + ": " + str(e))
|
||||||
|
|
||||||
|
async def _read_tcp_messages(self):
|
||||||
|
self.writer.write(self.login_code)
|
||||||
|
await self.writer.drain()
|
||||||
|
await self.reader.read(1000)
|
||||||
|
self.logged_in = True
|
||||||
|
while not self.shutting_down:
|
||||||
|
data = await self.reader.read(1000)
|
||||||
|
if len(data) == 0:
|
||||||
|
self.logged_in = False
|
||||||
|
raise LostConnection
|
||||||
|
while len(data) >= 12:
|
||||||
|
packet_type = int(data[0])
|
||||||
|
packet_length = struct.unpack(">I", data[1:5])[0]
|
||||||
|
packet = data[5:packet_length+5]
|
||||||
|
try:
|
||||||
|
if packet_length == len(packet):
|
||||||
|
if packet_type == 115:
|
||||||
|
switch_id = str(struct.unpack(">I", packet[0:4])[0])
|
||||||
|
home_id = self.switchID_to_homeID[switch_id]
|
||||||
|
|
||||||
|
#send response packet
|
||||||
|
response_id = struct.unpack(">H", packet[4:6])[0]
|
||||||
|
response_packet = bytes.fromhex('7300000007') + int(switch_id).to_bytes(4,'big') + response_id.to_bytes(2,'big') + bytes.fromhex('00')
|
||||||
|
self.loop.call_soon_threadsafe(self.send_request, response_packet)
|
||||||
|
|
||||||
|
if packet_length >= 33 and int(packet[13]) == 219:
|
||||||
|
#parse state and brightness change packet
|
||||||
|
deviceID = self.home_devices[home_id][int(packet[21])]
|
||||||
|
state = int(packet[27]) > 0
|
||||||
|
brightness = int(packet[28]) if state else 0
|
||||||
|
if deviceID in self.cync_switches:
|
||||||
|
self.cync_switches[deviceID].update_switch(state,brightness,self.cync_switches[deviceID].color_temp_kelvin,self.cync_switches[deviceID].rgb)
|
||||||
|
elif packet_length >= 25 and int(packet[13]) == 84:
|
||||||
|
#parse motion and ambient light sensor packet
|
||||||
|
deviceID = self.home_devices[home_id][int(packet[16])]
|
||||||
|
motion = int(packet[22]) > 0
|
||||||
|
ambient_light = int(packet[24]) > 0
|
||||||
|
if deviceID in self.cync_motion_sensors:
|
||||||
|
self.cync_motion_sensors[deviceID].update_motion_sensor(motion)
|
||||||
|
if deviceID in self.cync_ambient_light_sensors:
|
||||||
|
self.cync_ambient_light_sensors[deviceID].update_ambient_light_sensor(ambient_light)
|
||||||
|
elif packet_length > 51 and int(packet[13]) == 82:
|
||||||
|
#parse initial state packet
|
||||||
|
switch_id = str(struct.unpack(">I", packet[0:4])[0])
|
||||||
|
home_id = self.switchID_to_homeID[switch_id]
|
||||||
|
self._add_connected_devices(switch_id, home_id)
|
||||||
|
packet = packet[22:]
|
||||||
|
while len(packet) > 24:
|
||||||
|
deviceID = self.home_devices[home_id][int(packet[0])]
|
||||||
|
if deviceID in self.cync_switches:
|
||||||
|
if self.cync_switches[deviceID].elements > 1:
|
||||||
|
for i in range(self.cync_switches[deviceID].elements):
|
||||||
|
device_id = self.home_devices[home_id][(i+1)*256 + int(packet[0])]
|
||||||
|
state = int((int(packet[12]) >> i) & int(packet[8])) > 0
|
||||||
|
brightness = 100 if state else 0
|
||||||
|
self.cync_switches[device_id].update_switch(state,brightness,self.cync_switches[device_id].color_temp_kelvin,self.cync_switches[device_id].rgb)
|
||||||
|
else:
|
||||||
|
state = int(packet[8]) > 0
|
||||||
|
brightness = int(packet[12]) if state else 0
|
||||||
|
color_temp = int(packet[16])
|
||||||
|
rgb = {'r':int(packet[20]),'g':int(packet[21]),'b':int(packet[22]),'active':int(packet[16])==254}
|
||||||
|
self.cync_switches[deviceID].update_switch(state,brightness,color_temp,rgb)
|
||||||
|
packet = packet[24:]
|
||||||
|
elif packet_type == 131:
|
||||||
|
switch_id = str(struct.unpack(">I", packet[0:4])[0])
|
||||||
|
home_id = self.switchID_to_homeID[switch_id]
|
||||||
|
if packet_length >= 33 and int(packet[13]) == 219:
|
||||||
|
#parse state and brightness change packet
|
||||||
|
deviceID = self.home_devices[home_id][int(packet[21])]
|
||||||
|
state = int(packet[27]) > 0
|
||||||
|
brightness = int(packet[28]) if state else 0
|
||||||
|
if deviceID in self.cync_switches:
|
||||||
|
self.cync_switches[deviceID].update_switch(state,brightness,self.cync_switches[deviceID].color_temp_kelvin,self.cync_switches[deviceID].rgb)
|
||||||
|
elif packet_length >= 25 and int(packet[13]) == 84:
|
||||||
|
#parse motion and ambient light sensor packet
|
||||||
|
deviceID = self.home_devices[home_id][int(packet[16])]
|
||||||
|
motion = int(packet[22]) > 0
|
||||||
|
ambient_light = int(packet[24]) > 0
|
||||||
|
if deviceID in self.cync_motion_sensors:
|
||||||
|
self.cync_motion_sensors[deviceID].update_motion_sensor(motion)
|
||||||
|
if deviceID in self.cync_ambient_light_sensors:
|
||||||
|
self.cync_ambient_light_sensors[deviceID].update_ambient_light_sensor(ambient_light)
|
||||||
|
elif packet_type == 67 and packet_length >= 26 and int(packet[4]) == 1 and int(packet[5]) == 1 and int(packet[6]) == 6:
|
||||||
|
#parse state packet
|
||||||
|
switch_id = str(struct.unpack(">I", packet[0:4])[0])
|
||||||
|
home_id = self.switchID_to_homeID[switch_id]
|
||||||
|
packet = packet[7:]
|
||||||
|
while len(packet) >= 19:
|
||||||
|
if int(packet[3]) < len(self.home_devices[home_id]):
|
||||||
|
deviceID = self.home_devices[home_id][int(packet[3])]
|
||||||
|
if deviceID in self.cync_switches:
|
||||||
|
if self.cync_switches[deviceID].elements > 1:
|
||||||
|
for i in range(self.cync_switches[deviceID].elements):
|
||||||
|
device_id = self.home_devices[home_id][(i+1)*256 + int(packet[3])]
|
||||||
|
state = int((int(packet[5]) >> i) & int(packet[4])) > 0
|
||||||
|
brightness = 100 if state else 0
|
||||||
|
self.cync_switches[device_id].update_switch(state,brightness,self.cync_switches[device_id].color_temp_kelvin,self.cync_switches[device_id].rgb)
|
||||||
|
else:
|
||||||
|
state = int(packet[4]) > 0
|
||||||
|
brightness = int(packet[5]) if state else 0
|
||||||
|
color_temp = int(packet[6])
|
||||||
|
rgb = {'r':int(packet[7]),'g':int(packet[8]),'b':int(packet[9]),'active':int(packet[6])==254}
|
||||||
|
self.cync_switches[deviceID].update_switch(state,brightness,color_temp,rgb)
|
||||||
|
packet = packet[19:]
|
||||||
|
elif packet_type == 171:
|
||||||
|
switch_id = str(struct.unpack(">I", packet[0:4])[0])
|
||||||
|
home_id = self.switchID_to_homeID[switch_id]
|
||||||
|
self._add_connected_devices(switch_id, home_id)
|
||||||
|
elif packet_type == 123:
|
||||||
|
seq = str(struct.unpack(">H", packet[4:6])[0])
|
||||||
|
command_received = self.pending_commands.get(seq,None)
|
||||||
|
if command_received is not None:
|
||||||
|
command_received(seq)
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(str(type(e).__name__) + ": " + str(e))
|
||||||
|
data = data[packet_length+5:]
|
||||||
|
raise ShuttingDown
|
||||||
|
|
||||||
|
|
||||||
|
async def _maintain_connection(self):
|
||||||
|
while not self.shutting_down:
|
||||||
|
await asyncio.sleep(180)
|
||||||
|
self.writer.write(bytes.fromhex('d300000000'))
|
||||||
|
await self.writer.drain()
|
||||||
|
raise ShuttingDown
|
||||||
|
|
||||||
|
def _add_connected_devices(self,switch_id, home_id):
|
||||||
|
for dev in self.switchID_to_deviceIDs[switch_id]:
|
||||||
|
#update list of WiFi connected devices
|
||||||
|
if dev not in self.connected_devices[home_id]:
|
||||||
|
self.connected_devices[home_id].append(dev)
|
||||||
|
if self.connected_devices_updated:
|
||||||
|
for dev in self.cync_switches.values():
|
||||||
|
dev.update_controllers()
|
||||||
|
for room in self.cync_rooms.values():
|
||||||
|
room.update_controllers()
|
||||||
|
|
||||||
|
async def _update_connected_devices(self):
|
||||||
|
while not self.shutting_down:
|
||||||
|
self.connected_devices_updated = False
|
||||||
|
for devices in self.connected_devices.values():
|
||||||
|
devices.clear()
|
||||||
|
while not self.logged_in:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
attempts = 0
|
||||||
|
while True in [len(devices) < len(self.home_controllers[home_id]) * 0.5 for home_id,devices in self.connected_devices.items()] and attempts < 10:
|
||||||
|
for home_id, home_controllers in self.home_controllers.items():
|
||||||
|
for controller in home_controllers:
|
||||||
|
seq = self.get_seq_num()
|
||||||
|
ping = bytes.fromhex('a300000007') + int(controller).to_bytes(4,'big') + seq.to_bytes(2,'big') + bytes.fromhex('00')
|
||||||
|
self.loop.call_soon_threadsafe(self.send_request, ping)
|
||||||
|
await asyncio.sleep(0.15)
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
attempts += 1
|
||||||
|
for dev in self.cync_switches.values():
|
||||||
|
dev.update_controllers()
|
||||||
|
for room in self.cync_rooms.values():
|
||||||
|
room.update_controllers()
|
||||||
|
self.connected_devices_updated = True
|
||||||
|
await asyncio.sleep(3600)
|
||||||
|
raise ShuttingDown
|
||||||
|
|
||||||
|
async def _update_state(self):
|
||||||
|
while not self.connected_devices_updated:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
for connected_devices in self.connected_devices.values():
|
||||||
|
if len(connected_devices) > 0:
|
||||||
|
controller = self.cync_switches[connected_devices[0]].switch_id
|
||||||
|
seq = self.get_seq_num()
|
||||||
|
state_request = bytes.fromhex('7300000018') + int(controller).to_bytes(4,'big') + seq.to_bytes(2,'big') + bytes.fromhex('007e00000000f85206000000ffff0000567e')
|
||||||
|
self.loop.call_soon_threadsafe(self.send_request,state_request)
|
||||||
|
while False in [self.cync_switches[dev_id]._update_callback is not None for dev_id in self.options["switches"]] and False in [self.cync_rooms[dev_id]._update_callback is not None for dev_id in self.options["rooms"]]:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
for dev in self.cync_switches.values():
|
||||||
|
dev.publish_update()
|
||||||
|
for room in self.cync_rooms.values():
|
||||||
|
dev.publish_update()
|
||||||
|
|
||||||
|
def send_request(self,request):
|
||||||
|
async def send():
|
||||||
|
self.writer.write(request)
|
||||||
|
await self.writer.drain()
|
||||||
|
self.loop.create_task(send())
|
||||||
|
|
||||||
|
def combo_control(self,state,brightness,color_tone,rgb,switch_id,mesh_id,seq):
|
||||||
|
combo_request = bytes.fromhex('7300000022') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8f010000000000000') + mesh_id + bytes.fromhex('f00000') + (1 if state else 0).to_bytes(1,'big') + brightness.to_bytes(1,'big') + color_tone.to_bytes(1,'big') + rgb[0].to_bytes(1,'big') + rgb[1].to_bytes(1,'big') + rgb[2].to_bytes(1,'big') + ((496 + int(mesh_id[0]) + int(mesh_id[1]) + (1 if state else 0) + brightness + color_tone + sum(rgb))%256).to_bytes(1,'big') + bytes.fromhex('7e')
|
||||||
|
self.loop.call_soon_threadsafe(self.send_request,combo_request)
|
||||||
|
|
||||||
|
def turn_on(self,switch_id,mesh_id,seq):
|
||||||
|
power_request = bytes.fromhex('730000001f') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8d00d000000000000') + mesh_id + bytes.fromhex('d00000010000') + ((430 + int(mesh_id[0]) + int(mesh_id[1]))%256).to_bytes(1,'big') + bytes.fromhex('7e')
|
||||||
|
self.loop.call_soon_threadsafe(self.send_request,power_request)
|
||||||
|
|
||||||
|
def turn_off(self,switch_id,mesh_id,seq):
|
||||||
|
power_request = bytes.fromhex('730000001f') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8d00d000000000000') + mesh_id + bytes.fromhex('d00000000000') + ((429 + int(mesh_id[0]) + int(mesh_id[1]))%256).to_bytes(1,'big') + bytes.fromhex('7e')
|
||||||
|
self.loop.call_soon_threadsafe(self.send_request,power_request)
|
||||||
|
|
||||||
|
def set_color_temp(self,color_temp,switch_id,mesh_id,seq):
|
||||||
|
color_temp_request = bytes.fromhex('730000001e') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8e20c000000000000') + mesh_id + bytes.fromhex('e2000005') + color_temp.to_bytes(1,'big') + ((469 + int(mesh_id[0]) + int(mesh_id[1]) + color_temp)%256).to_bytes(1,'big') + bytes.fromhex('7e')
|
||||||
|
self.loop.call_soon_threadsafe(self.send_request,color_temp_request)
|
||||||
|
|
||||||
|
def get_seq_num(self):
|
||||||
|
if self._seq_num == 65535:
|
||||||
|
self._seq_num = 1
|
||||||
|
else:
|
||||||
|
self._seq_num += 1
|
||||||
|
return self._seq_num
|
||||||
|
|
||||||
|
class CyncRoom:
|
||||||
|
|
||||||
|
def __init__(self, room_id, room_info, hub):
|
||||||
|
|
||||||
|
self.hub = hub
|
||||||
|
self.room_id = room_id
|
||||||
|
self.home_id = room_id.split('-')[0]
|
||||||
|
self.name = room_info.get('name','unknown')
|
||||||
|
self.home_name = room_info.get('home_name','unknown')
|
||||||
|
self.parent_room = room_info.get('parent_room', 'unknown')
|
||||||
|
self.mesh_id = int(room_info.get('mesh_id',0)).to_bytes(2,'little')
|
||||||
|
self.power_state = False
|
||||||
|
self.brightness = 0
|
||||||
|
self.color_temp_kelvin = 0
|
||||||
|
self.rgb = {'r':0, 'g':0, 'b':0, 'active': False}
|
||||||
|
self.switches = room_info.get('switches',[])
|
||||||
|
self.subgroups = room_info.get('subgroups',[])
|
||||||
|
self.is_subgroup = room_info.get('isSubgroup', False)
|
||||||
|
self.all_room_switches = self.switches
|
||||||
|
self.controllers = []
|
||||||
|
self.default_controller = room_info.get('room_controller',self.hub.home_controllers[self.home_id][0])
|
||||||
|
self._update_callback = None
|
||||||
|
self._update_parent_room = None
|
||||||
|
self.support_brightness = False
|
||||||
|
self.support_color_temp = False
|
||||||
|
self.support_rgb = False
|
||||||
|
self.switches_support_brightness = False
|
||||||
|
self.switches_support_color_temp = False
|
||||||
|
self.switches_support_rgb = False
|
||||||
|
self.groups_support_brightness = False
|
||||||
|
self.groups_support_color_temp = False
|
||||||
|
self.groups_support_rgb = False
|
||||||
|
self._command_timout = 0.5
|
||||||
|
self._command_retry_time = 5
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""Initialization of supported features and registration of update function for all switches and subgroups in the room"""
|
||||||
|
self.switches_support_brightness = [device_id for device_id in self.switches if self.hub.cync_switches[device_id].support_brightness]
|
||||||
|
self.switches_support_color_temp = [device_id for device_id in self.switches if self.hub.cync_switches[device_id].support_color_temp]
|
||||||
|
self.switches_support_rgb = [device_id for device_id in self.switches if self.hub.cync_switches[device_id].support_rgb]
|
||||||
|
self.groups_support_brightness = [room_id for room_id in self.subgroups if self.hub.cync_rooms[room_id].support_brightness]
|
||||||
|
self.groups_support_color_temp = [room_id for room_id in self.subgroups if self.hub.cync_rooms[room_id].support_color_temp]
|
||||||
|
self.groups_support_rgb = [room_id for room_id in self.subgroups if self.hub.cync_rooms[room_id].support_rgb]
|
||||||
|
self.support_brightness = (len(self.switches_support_brightness) + len(self.groups_support_brightness)) > 0
|
||||||
|
self.support_color_temp = (len(self.switches_support_color_temp) + len(self.groups_support_color_temp)) > 0
|
||||||
|
self.support_rgb = (len(self.switches_support_rgb) + len(self.groups_support_rgb)) > 0
|
||||||
|
for switch_id in self.switches:
|
||||||
|
self.hub.cync_switches[switch_id].register_room_updater(self.update_room)
|
||||||
|
for subgroup in self.subgroups:
|
||||||
|
self.hub.cync_rooms[subgroup].register_room_updater(self.update_room)
|
||||||
|
self.all_room_switches = self.all_room_switches + self.hub.cync_rooms[subgroup].switches
|
||||||
|
for subgroup in self.subgroups:
|
||||||
|
self.hub.cync_rooms[subgroup].all_room_switches = self.all_room_switches
|
||||||
|
|
||||||
|
def register(self, update_callback) -> None:
|
||||||
|
"""Register callback, called when switch changes state."""
|
||||||
|
self._update_callback = update_callback
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Remove previously registered callback."""
|
||||||
|
self._update_callback = None
|
||||||
|
|
||||||
|
def register_room_updater(self, parent_updater):
|
||||||
|
self._update_parent_room = parent_updater
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_color_temp_kelvin(self) -> int:
|
||||||
|
"""Return maximum supported color temperature."""
|
||||||
|
return 7000
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_color_temp_kelvin(self) -> int:
|
||||||
|
"""Return minimum supported color temperature."""
|
||||||
|
return 2000
|
||||||
|
|
||||||
|
async def turn_on(self, attr_rgb, attr_br, attr_ct) -> None:
|
||||||
|
"""Turn on the light."""
|
||||||
|
attempts = 0
|
||||||
|
update_received = False
|
||||||
|
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
|
||||||
|
seq = str(self.hub.get_seq_num())
|
||||||
|
if len(self.controllers) > 0:
|
||||||
|
controller = self.controllers[attempts%len(self.controllers)]
|
||||||
|
else:
|
||||||
|
controller = self.default_controller
|
||||||
|
if attr_rgb is not None and attr_br is not None:
|
||||||
|
if math.isclose(attr_br, max([self.rgb['r'],self.rgb['g'],self.rgb['b']])*self.brightness/100, abs_tol = 2):
|
||||||
|
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
|
||||||
|
else:
|
||||||
|
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
|
||||||
|
elif attr_rgb is None and attr_ct is None and attr_br is not None:
|
||||||
|
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
|
||||||
|
elif attr_rgb is not None and attr_br is None:
|
||||||
|
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
|
||||||
|
elif attr_ct is not None:
|
||||||
|
self.hub.turn_on(controller, self.mesh_id, seq)
|
||||||
|
# Sync wants the color temp as a percentage of the range. So we need to
|
||||||
|
# calculate what percentage of the color temp range is being requested
|
||||||
|
# before sending it to the server.
|
||||||
|
color_temp = round(
|
||||||
|
(
|
||||||
|
(attr_ct - self.min_color_temp_kelvin) /
|
||||||
|
self.max_color_temp_kelvin
|
||||||
|
) * 100
|
||||||
|
)
|
||||||
|
self.hub.set_color_temp(color_temp, controller, self.mesh_id, seq)
|
||||||
|
else:
|
||||||
|
self.hub.turn_on(controller, self.mesh_id, seq)
|
||||||
|
self.hub.pending_commands[seq] = self.command_received
|
||||||
|
await asyncio.sleep(self._command_timout)
|
||||||
|
if self.hub.pending_commands.get(seq, None) is not None:
|
||||||
|
self.hub.pending_commands.pop(seq)
|
||||||
|
attempts += 1
|
||||||
|
else:
|
||||||
|
update_received = True
|
||||||
|
|
||||||
|
async def turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the light."""
|
||||||
|
attempts = 0
|
||||||
|
update_received = False
|
||||||
|
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
|
||||||
|
seq = str(self.hub.get_seq_num())
|
||||||
|
if len(self.controllers) > 0:
|
||||||
|
controller = self.controllers[attempts%len(self.controllers)]
|
||||||
|
else:
|
||||||
|
controller = self.default_controller
|
||||||
|
self.hub.turn_off(controller, self.mesh_id, seq)
|
||||||
|
self.hub.pending_commands[seq] = self.command_received
|
||||||
|
await asyncio.sleep(self._command_timout)
|
||||||
|
if self.hub.pending_commands.get(seq, None) is not None:
|
||||||
|
self.hub.pending_commands.pop(seq)
|
||||||
|
attempts += 1
|
||||||
|
else:
|
||||||
|
update_received = True
|
||||||
|
|
||||||
|
def command_received(self, seq):
|
||||||
|
"""Remove command from hub.pending_commands when a reply is received from Cync server"""
|
||||||
|
if self.hub.pending_commands.get(seq) is not None:
|
||||||
|
self.hub.pending_commands.pop(seq)
|
||||||
|
|
||||||
|
def update_room(self):
|
||||||
|
"""Update the current state of the room"""
|
||||||
|
_brightness = self.brightness
|
||||||
|
_color_temp = self.color_temp_kelvin
|
||||||
|
_rgb = self.rgb
|
||||||
|
_power_state = True in ([self.hub.cync_switches[device_id].power_state for device_id in self.switches] + [self.hub.cync_rooms[room_id].power_state for room_id in self.subgroups])
|
||||||
|
if self.support_brightness:
|
||||||
|
_brightness = round(sum([self.hub.cync_switches[device_id].brightness for device_id in self.switches] + [self.hub.cync_rooms[room_id].brightness for room_id in self.subgroups])/(len(self.switches) + len(self.subgroups)))
|
||||||
|
else:
|
||||||
|
_brightness = 100 if _power_state else 0
|
||||||
|
if self.support_color_temp:
|
||||||
|
_color_temp = round(sum([self.hub.cync_switches[device_id].color_temp_kelvin for device_id in self.switches_support_color_temp] + [self.hub.cync_rooms[room_id].color_temp_kelvin for room_id in self.groups_support_color_temp])/(len(self.switches_support_color_temp) + len(self.groups_support_color_temp)))
|
||||||
|
if self.support_rgb:
|
||||||
|
_rgb['r'] = round(sum([self.hub.cync_switches[device_id].rgb['r'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['r'] for room_id in self.groups_support_rgb])/(len(self.switches_support_rgb) + len(self.groups_support_rgb)))
|
||||||
|
_rgb['g'] = round(sum([self.hub.cync_switches[device_id].rgb['g'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['g'] for room_id in self.groups_support_rgb])/(len(self.switches_support_rgb) + len(self.groups_support_rgb)))
|
||||||
|
_rgb['b'] = round(sum([self.hub.cync_switches[device_id].rgb['b'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['b'] for room_id in self.groups_support_rgb])/(len(self.switches_support_rgb) + len(self.groups_support_rgb)))
|
||||||
|
_rgb['active'] = True in ([self.hub.cync_switches[device_id].rgb['active'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['active'] for room_id in self.groups_support_rgb])
|
||||||
|
if _power_state != self.power_state or _brightness != self.brightness or _color_temp != self.color_temp_kelvin or _rgb != self.rgb:
|
||||||
|
self.power_state = _power_state
|
||||||
|
self.brightness = _brightness
|
||||||
|
self.color_temp_kelvin = _color_temp
|
||||||
|
self.rgb = _rgb
|
||||||
|
self.publish_update()
|
||||||
|
if self._update_parent_room:
|
||||||
|
self._update_parent_room()
|
||||||
|
|
||||||
|
def update_controllers(self):
|
||||||
|
"""Update the list of responsive, Wi-Fi connected controller devices"""
|
||||||
|
connected_devices = self.hub.connected_devices[self.home_id]
|
||||||
|
controllers = []
|
||||||
|
if len(connected_devices) > 0:
|
||||||
|
controllers = [self.hub.cync_switches[dev_id].switch_id for dev_id in self.all_room_switches if dev_id in connected_devices]
|
||||||
|
others_available = [self.hub.cync_switches[dev_id].switch_id for dev_id in connected_devices]
|
||||||
|
for controller in controllers:
|
||||||
|
if controller in others_available:
|
||||||
|
others_available.remove(controller)
|
||||||
|
self.controllers = controllers + others_available
|
||||||
|
else:
|
||||||
|
self.controllers = [self.default_controller]
|
||||||
|
|
||||||
|
def publish_update(self):
|
||||||
|
if self._update_callback:
|
||||||
|
self._update_callback()
|
||||||
|
|
||||||
|
class CyncSwitch:
|
||||||
|
|
||||||
|
def __init__(self, device_id, switch_info, room, hub):
|
||||||
|
self.hub = hub
|
||||||
|
self.device_id = device_id
|
||||||
|
self.switch_id = switch_info.get('switch_id','0')
|
||||||
|
self.home_id = [home_id for home_id, home_devices in self.hub.home_devices.items() if self.device_id in home_devices][0]
|
||||||
|
self.name = switch_info.get('name','unknown')
|
||||||
|
self.home_name = switch_info.get('home_name','unknown')
|
||||||
|
self.mesh_id = switch_info.get('mesh_id',0).to_bytes(2,'little')
|
||||||
|
self.room = room
|
||||||
|
self.power_state = False
|
||||||
|
self.brightness = 0
|
||||||
|
self.color_temp_kelvin = 0
|
||||||
|
self.rgb = {'r':0, 'g':0, 'b':0, 'active':False}
|
||||||
|
self.default_controller = switch_info.get('switch_controller',self.hub.home_controllers[self.home_id][0])
|
||||||
|
self.controllers = []
|
||||||
|
self._update_callback = None
|
||||||
|
self._update_parent_room = None
|
||||||
|
self.support_brightness = switch_info.get('BRIGHTNESS',False)
|
||||||
|
self.support_color_temp = switch_info.get('COLORTEMP',False)
|
||||||
|
self.support_rgb = switch_info.get('RGB',False)
|
||||||
|
self.plug = switch_info.get('PLUG',False)
|
||||||
|
self.fan = switch_info.get('FAN',False)
|
||||||
|
self.elements = switch_info.get('MULTIELEMENT',1)
|
||||||
|
self._command_timout = 0.5
|
||||||
|
self._command_retry_time = 5
|
||||||
|
|
||||||
|
def register(self, update_callback) -> None:
|
||||||
|
"""Register callback, called when switch changes state."""
|
||||||
|
self._update_callback = update_callback
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Remove previously registered callback."""
|
||||||
|
self._update_callback = None
|
||||||
|
|
||||||
|
def register_room_updater(self, parent_updater):
|
||||||
|
self._update_parent_room = parent_updater
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_color_temp_kelvin(self) -> int:
|
||||||
|
"""Return maximum supported color temperature."""
|
||||||
|
return 7000
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_color_temp_kelvin(self) -> int:
|
||||||
|
"""Return minimum supported color temperature."""
|
||||||
|
return 2000
|
||||||
|
|
||||||
|
async def turn_on(self, attr_rgb, attr_br, attr_ct) -> None:
|
||||||
|
"""Turn on the light."""
|
||||||
|
attempts = 0
|
||||||
|
update_received = False
|
||||||
|
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
|
||||||
|
seq = str(self.hub.get_seq_num())
|
||||||
|
if len(self.controllers) > 0:
|
||||||
|
controller = self.controllers[attempts%len(self.controllers)]
|
||||||
|
else:
|
||||||
|
controller = self.default_controller
|
||||||
|
if attr_rgb is not None and attr_br is not None:
|
||||||
|
if math.isclose(attr_br, max([self.rgb['r'],self.rgb['g'],self.rgb['b']])*self.brightness/100, abs_tol = 2):
|
||||||
|
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
|
||||||
|
else:
|
||||||
|
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
|
||||||
|
elif attr_rgb is None and attr_ct is None and attr_br is not None:
|
||||||
|
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
|
||||||
|
elif attr_rgb is not None and attr_br is None:
|
||||||
|
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
|
||||||
|
elif attr_ct is not None:
|
||||||
|
# Sync wants the color temp as a percentage of the range. So we need to
|
||||||
|
# calculate what percentage of the color temp range is being requested
|
||||||
|
# before sending it to the server.
|
||||||
|
color_temp = round(
|
||||||
|
(
|
||||||
|
(attr_ct - self.min_color_temp_kelvin) /
|
||||||
|
self.max_color_temp_kelvin
|
||||||
|
) * 100
|
||||||
|
)
|
||||||
|
self.hub.set_color_temp(color_temp, controller, self.mesh_id, seq)
|
||||||
|
else:
|
||||||
|
self.hub.turn_on(controller, self.mesh_id, seq)
|
||||||
|
self.hub.pending_commands[seq] = self.command_received
|
||||||
|
await asyncio.sleep(self._command_timout)
|
||||||
|
if self.hub.pending_commands.get(seq, None) is not None:
|
||||||
|
self.hub.pending_commands.pop(seq)
|
||||||
|
attempts += 1
|
||||||
|
else:
|
||||||
|
update_received = True
|
||||||
|
|
||||||
|
async def turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the light."""
|
||||||
|
attempts = 0
|
||||||
|
update_received = False
|
||||||
|
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
|
||||||
|
seq = str(self.hub.get_seq_num())
|
||||||
|
if len(self.controllers) > 0:
|
||||||
|
controller = self.controllers[attempts%len(self.controllers)]
|
||||||
|
else:
|
||||||
|
controller = self.default_controller
|
||||||
|
self.hub.turn_off(controller, self.mesh_id, seq)
|
||||||
|
self.hub.pending_commands[seq] = self.command_received
|
||||||
|
await asyncio.sleep(self._command_timout)
|
||||||
|
if self.hub.pending_commands.get(seq, None) is not None:
|
||||||
|
self.hub.pending_commands.pop(seq)
|
||||||
|
attempts += 1
|
||||||
|
else:
|
||||||
|
update_received = True
|
||||||
|
|
||||||
|
def command_received(self, seq):
|
||||||
|
"""Remove command from hub.pending_commands when a reply is received from Cync server"""
|
||||||
|
if self.hub.pending_commands.get(seq) is not None:
|
||||||
|
self.hub.pending_commands.pop(seq)
|
||||||
|
|
||||||
|
def update_switch(self,state,brightness,color_temp,rgb):
|
||||||
|
"""Update the state of the switch as updates are received from the Cync server"""
|
||||||
|
self.update_received = True
|
||||||
|
# Cync sends the color temp as a percentage from 0-100 based on the max and min
|
||||||
|
# color temp. Most Cync bulbs support 2000K-7000K, so we have to calculate what
|
||||||
|
# is actually being requested.
|
||||||
|
_color_temp = round(
|
||||||
|
(self.max_color_temp_kelvin - self.min_color_temp_kelvin) *
|
||||||
|
(color_temp / 100) +
|
||||||
|
self.min_color_temp_kelvin
|
||||||
|
)
|
||||||
|
if self.power_state != state or self.brightness != brightness or self.color_temp_kelvin != _color_temp or self.rgb != rgb:
|
||||||
|
self.power_state = state
|
||||||
|
self.brightness = brightness if self.support_brightness and state else 100 if state else 0
|
||||||
|
# Cync sends the color temp as a percentage from 0-100 based on the max and
|
||||||
|
# min color temp. Most Cync bulbs support 2000K-7000K, so we have to
|
||||||
|
# calculate what is actually being requested.
|
||||||
|
self.color_temp_kelvin = _color_temp
|
||||||
|
self.rgb = rgb
|
||||||
|
self.publish_update()
|
||||||
|
if self._update_parent_room:
|
||||||
|
self._update_parent_room()
|
||||||
|
|
||||||
|
def update_controllers(self):
|
||||||
|
"""Update the list of responsive, Wi-Fi connected controller devices"""
|
||||||
|
connected_devices = self.hub.connected_devices[self.home_id]
|
||||||
|
controllers = []
|
||||||
|
if len(connected_devices) > 0:
|
||||||
|
if int(self.switch_id) > 0:
|
||||||
|
if self.device_id in connected_devices:
|
||||||
|
#if this device is connected, make this the first available controller
|
||||||
|
controllers.append(self.switch_id)
|
||||||
|
if self.room:
|
||||||
|
controllers = controllers + [self.hub.cync_switches[device_id].switch_id for device_id in self.room.all_room_switches if device_id in connected_devices and device_id != self.device_id]
|
||||||
|
others_available = [self.hub.cync_switches[device_id].switch_id for device_id in connected_devices]
|
||||||
|
for controller in controllers:
|
||||||
|
if controller in others_available:
|
||||||
|
others_available.remove(controller)
|
||||||
|
self.controllers = controllers + others_available
|
||||||
|
else:
|
||||||
|
self.controllers = [self.default_controller]
|
||||||
|
|
||||||
|
def publish_update(self):
|
||||||
|
if self._update_callback:
|
||||||
|
self._update_callback()
|
||||||
|
|
||||||
|
class CyncMotionSensor:
|
||||||
|
|
||||||
|
def __init__(self, device_id, device_info, room):
|
||||||
|
|
||||||
|
self.device_id = device_id
|
||||||
|
self.name = device_info['name']
|
||||||
|
self.home_name = device_info['home_name']
|
||||||
|
self.room = room
|
||||||
|
self.motion = False
|
||||||
|
self._update_callback = None
|
||||||
|
|
||||||
|
def register(self, update_callback) -> None:
|
||||||
|
"""Register callback, called when switch changes state."""
|
||||||
|
self._update_callback = update_callback
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Remove previously registered callback."""
|
||||||
|
self._update_callback = None
|
||||||
|
|
||||||
|
def update_motion_sensor(self,motion):
|
||||||
|
self.motion = motion
|
||||||
|
self.publish_update()
|
||||||
|
|
||||||
|
def publish_update(self):
|
||||||
|
if self._update_callback:
|
||||||
|
self._update_callback()
|
||||||
|
|
||||||
|
class CyncAmbientLightSensor:
|
||||||
|
|
||||||
|
def __init__(self, device_id, device_info, room):
|
||||||
|
|
||||||
|
self.device_id = device_id
|
||||||
|
self.name = device_info['name']
|
||||||
|
self.home_name = device_info['home_name']
|
||||||
|
self.room = room
|
||||||
|
self.ambient_light = False
|
||||||
|
self._update_callback = None
|
||||||
|
|
||||||
|
def register(self, update_callback) -> None:
|
||||||
|
"""Register callback, called when switch changes state."""
|
||||||
|
self._update_callback = update_callback
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Remove previously registered callback."""
|
||||||
|
self._update_callback = None
|
||||||
|
|
||||||
|
def update_ambient_light_sensor(self,ambient_light):
|
||||||
|
self.ambient_light = ambient_light
|
||||||
|
self.publish_update()
|
||||||
|
|
||||||
|
def publish_update(self):
|
||||||
|
if self._update_callback:
|
||||||
|
self._update_callback()
|
||||||
|
|
||||||
|
class CyncUserData:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.username = ''
|
||||||
|
self.password = ''
|
||||||
|
self.auth_code = None
|
||||||
|
self.user_credentials = {}
|
||||||
|
|
||||||
|
async def authenticate(self,username,password):
|
||||||
|
"""Authenticate with the API and get a token."""
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
auth_data = {'corp_id': "1007d2ad150c4000", 'email': self.username, 'password': self.password}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(API_AUTH, json=auth_data) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
self.user_credentials = await resp.json()
|
||||||
|
login_code = bytearray.fromhex('13000000') + (10 + len(self.user_credentials['authorize'])).to_bytes(1,'big') + bytearray.fromhex('03') + self.user_credentials['user_id'].to_bytes(4,'big') + len(self.user_credentials['authorize']).to_bytes(2,'big') + bytearray(self.user_credentials['authorize'],'ascii') + bytearray.fromhex('0000b4')
|
||||||
|
self.auth_code = [int.from_bytes([byt],'big') for byt in login_code]
|
||||||
|
return {'authorized':True}
|
||||||
|
elif resp.status == 400:
|
||||||
|
request_code_data = {'corp_id': "1007d2ad150c4000", 'email': self.username, 'local_lang': "en-us"}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(API_REQUEST_CODE,json=request_code_data) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
return {'authorized':False,'two_factor_code_required':True}
|
||||||
|
else:
|
||||||
|
return {'authorized':False,'two_factor_code_required':False}
|
||||||
|
else:
|
||||||
|
return {'authorized':False,'two_factor_code_required':False}
|
||||||
|
|
||||||
|
async def auth_two_factor(self, code):
|
||||||
|
"""Authenticate with 2 Factor Code."""
|
||||||
|
two_factor_data = {'corp_id': "1007d2ad150c4000", 'email': self.username,'password': self.password, 'two_factor': code, 'resource':"abcdefghijklmnop"}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(API_2FACTOR_AUTH,json=two_factor_data) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
self.user_credentials = await resp.json()
|
||||||
|
login_code = bytearray.fromhex('13000000') + (10 + len(self.user_credentials['authorize'])).to_bytes(1,'big') + bytearray.fromhex('03') + self.user_credentials['user_id'].to_bytes(4,'big') + len(self.user_credentials['authorize']).to_bytes(2,'big') + bytearray(self.user_credentials['authorize'],'ascii') + bytearray.fromhex('0000b4')
|
||||||
|
self.auth_code = [int.from_bytes([byt],'big') for byt in login_code]
|
||||||
|
return {'authorized':True}
|
||||||
|
else:
|
||||||
|
return {'authorized':False}
|
||||||
|
|
||||||
|
async def get_cync_config(self):
|
||||||
|
home_devices = {}
|
||||||
|
home_controllers = {}
|
||||||
|
switchID_to_homeID = {}
|
||||||
|
devices = {}
|
||||||
|
rooms = {}
|
||||||
|
homes = await self._get_homes()
|
||||||
|
for home in homes:
|
||||||
|
home_info = await self._get_home_properties(home['product_id'], home['id'])
|
||||||
|
if home_info.get('groupsArray',False) and home_info.get('bulbsArray',False) and len(home_info['groupsArray']) > 0 and len(home_info['bulbsArray']) > 0:
|
||||||
|
home_id = str(home['id'])
|
||||||
|
bulbs_array_length = max([((device['deviceID'] % home['id']) % 1000) + (int((device['deviceID'] % home['id']) / 1000)*256) for device in home_info['bulbsArray']]) + 1
|
||||||
|
home_devices[home_id] = [""]*(bulbs_array_length)
|
||||||
|
home_controllers[home_id] = []
|
||||||
|
for device in home_info['bulbsArray']:
|
||||||
|
device_type = device['deviceType']
|
||||||
|
device_id = str(device['deviceID'])
|
||||||
|
current_index = ((device['deviceID'] % home['id']) % 1000) + (int((device['deviceID'] % home['id']) / 1000)*256)
|
||||||
|
home_devices[home_id][current_index] = device_id
|
||||||
|
devices[device_id] = {'name':device['displayName'],
|
||||||
|
'mesh_id':current_index,
|
||||||
|
'switch_id':str(device.get('switchID',0)),
|
||||||
|
'ONOFF': device_type in Capabilities['ONOFF'],
|
||||||
|
'BRIGHTNESS': device_type in Capabilities["BRIGHTNESS"],
|
||||||
|
"COLORTEMP":device_type in Capabilities["COLORTEMP"],
|
||||||
|
"RGB": device_type in Capabilities["RGB"],
|
||||||
|
"MOTION": device_type in Capabilities["MOTION"],
|
||||||
|
"AMBIENT_LIGHT": device_type in Capabilities["AMBIENT_LIGHT"],
|
||||||
|
"WIFICONTROL": device_type in Capabilities["WIFICONTROL"],
|
||||||
|
"PLUG" : device_type in Capabilities["PLUG"],
|
||||||
|
"FAN" : device_type in Capabilities["FAN"],
|
||||||
|
'home_name':home['name'],
|
||||||
|
'room':'',
|
||||||
|
'room_name':''
|
||||||
|
}
|
||||||
|
if str(device_type) in Capabilities['MULTIELEMENT'] and current_index < 256:
|
||||||
|
devices[device_id]['MULTIELEMENT'] = Capabilities['MULTIELEMENT'][str(device_type)]
|
||||||
|
if devices[device_id].get('WIFICONTROL',False) and 'switchID' in device and device['switchID'] > 0:
|
||||||
|
switchID_to_homeID[str(device['switchID'])] = home_id
|
||||||
|
devices[device_id]['switch_controller'] = device['switchID']
|
||||||
|
home_controllers[home_id].append(device['switchID'])
|
||||||
|
if len(home_controllers[home_id]) == 0:
|
||||||
|
for device in home_info['bulbsArray']:
|
||||||
|
device_id = str(device['deviceID'])
|
||||||
|
devices.pop(device_id,'')
|
||||||
|
home_devices.pop(home_id,'')
|
||||||
|
home_controllers.pop(home_id,'')
|
||||||
|
else:
|
||||||
|
for room in home_info['groupsArray']:
|
||||||
|
if (len(room.get('deviceIDArray',[])) + len(room.get('subgroupIDArray',[]))) > 0:
|
||||||
|
room_id = home_id + '-' + str(room['groupID'])
|
||||||
|
room_controller = home_controllers[home_id][0]
|
||||||
|
available_room_controllers = [(id%1000) + (int(id/1000)*256) for id in room.get('deviceIDArray',[]) if 'switch_controller' in devices[home_devices[home_id][(id%1000)+(int(id/1000)*256)]]]
|
||||||
|
if len(available_room_controllers) > 0:
|
||||||
|
room_controller = devices[home_devices[home_id][available_room_controllers[0]]]['switch_controller']
|
||||||
|
for id in room.get('deviceIDArray',[]):
|
||||||
|
id = (id % 1000) + (int(id / 1000)*256)
|
||||||
|
devices[home_devices[home_id][id]]['room'] = room_id
|
||||||
|
devices[home_devices[home_id][id]]['room_name'] = room['displayName']
|
||||||
|
if 'switch_controller' not in devices[home_devices[home_id][id]] and devices[home_devices[home_id][id]].get('ONOFF',False):
|
||||||
|
devices[home_devices[home_id][id]]['switch_controller'] = room_controller
|
||||||
|
rooms[room_id] = {'name':room['displayName'],
|
||||||
|
'mesh_id' : room['groupID'],
|
||||||
|
'room_controller' : room_controller,
|
||||||
|
'home_name' : home['name'],
|
||||||
|
'switches' : [home_devices[home_id][(i%1000)+(int(i/1000)*256)] for i in room.get('deviceIDArray',[]) if devices[home_devices[home_id][(i%1000)+(int(i/1000)*256)]].get('ONOFF',False)],
|
||||||
|
'isSubgroup' : room.get('isSubgroup',False),
|
||||||
|
'subgroups' : [home_id + '-' + str(subgroup) for subgroup in room.get('subgroupIDArray',[])]
|
||||||
|
}
|
||||||
|
for room,room_info in rooms.items():
|
||||||
|
if not room_info.get("isSubgroup",False) and len(subgroups := room_info.get("subgroups",[]).copy()) > 0:
|
||||||
|
for subgroup in subgroups:
|
||||||
|
if rooms.get(subgroup,None):
|
||||||
|
rooms[subgroup]["parent_room"] = room_info["name"]
|
||||||
|
else:
|
||||||
|
room_info['subgroups'].pop(room_info['subgroups'].index(subgroup))
|
||||||
|
|
||||||
|
if len(rooms) == 0 or len(devices) == 0 or len(home_controllers) == 0 or len(home_devices) == 0 or len(switchID_to_homeID) == 0:
|
||||||
|
raise InvalidCyncConfiguration
|
||||||
|
else:
|
||||||
|
return {'rooms':rooms, 'devices':devices, 'home_devices':home_devices, 'home_controllers':home_controllers, 'switchID_to_homeID':switchID_to_homeID}
|
||||||
|
|
||||||
|
async def _get_homes(self):
|
||||||
|
"""Get a list of devices for a particular user."""
|
||||||
|
headers = {'Access-Token': self.user_credentials['access_token']}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(API_DEVICES.format(user=self.user_credentials['user_id']), headers=headers) as resp:
|
||||||
|
response = await resp.json()
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def _get_home_properties(self, product_id, device_id):
|
||||||
|
"""Get properties for a single device."""
|
||||||
|
headers = {'Access-Token': self.user_credentials['access_token']}
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(API_DEVICE_INFO.format(product_id=product_id, device_id=device_id), headers=headers) as resp:
|
||||||
|
response = await resp.json()
|
||||||
|
return response
|
||||||
|
|
||||||
|
class LostConnection(Exception):
|
||||||
|
"""Lost connection to Cync Server"""
|
||||||
|
|
||||||
|
class ShuttingDown(Exception):
|
||||||
|
"""Cync client shutting down"""
|
||||||
|
|
||||||
|
class InvalidCyncConfiguration(Exception):
|
||||||
|
"""Cync configuration is not supported"""
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""Platform for light integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from .const import DOMAIN
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
new_devices = []
|
||||||
|
for switch_id in hub.cync_switches:
|
||||||
|
if not hub.cync_switches[switch_id]._update_callback and hub.cync_switches[switch_id].fan and switch_id in config_entry.options["switches"]:
|
||||||
|
new_devices.append(CyncFanEntity(hub.cync_switches[switch_id]))
|
||||||
|
|
||||||
|
if new_devices:
|
||||||
|
async_add_entities(new_devices)
|
||||||
|
|
||||||
|
class CyncFanEntity(FanEntity):
|
||||||
|
"""Representation of a Cync Fan Switch Entity."""
|
||||||
|
|
||||||
|
should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, cync_switch) -> None:
|
||||||
|
"""Initialize the light."""
|
||||||
|
self.cync_switch = cync_switch
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when this Entity has been added to HA."""
|
||||||
|
self.cync_switch.register(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Entity being removed from hass."""
|
||||||
|
self.cync_switch.reset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device registry information for this entity."""
|
||||||
|
return DeviceInfo(
|
||||||
|
identifiers = {(DOMAIN, f"{self.cync_switch.room.name} ({self.cync_switch.home_name})")},
|
||||||
|
manufacturer = "Cync by Savant",
|
||||||
|
name = f"{self.cync_switch.room.name} ({self.cync_switch.home_name})",
|
||||||
|
suggested_area = f"{self.cync_switch.room.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return Unique ID string."""
|
||||||
|
return 'cync_switch_' + self.cync_switch.device_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the switch."""
|
||||||
|
return self.cync_switch.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self) -> int:
|
||||||
|
"""Return the list of supported features."""
|
||||||
|
return FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if fan is on."""
|
||||||
|
return self.cync_switch.power_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percentage(self) -> int | None:
|
||||||
|
"""Return the fan speed percentage of this switch"""
|
||||||
|
return self.cync_switch.brightness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speed_count(self) -> int:
|
||||||
|
"""Return the number of speeds the fan supports."""
|
||||||
|
return 4
|
||||||
|
|
||||||
|
async def async_turn_on(
|
||||||
|
self,
|
||||||
|
percentage: int | None = None,
|
||||||
|
preset_mode: str | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Turn on the light."""
|
||||||
|
await self.cync_switch.turn_on(None,percentage*255/100 if percentage is not None else None,None)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the light."""
|
||||||
|
await self.cync_switch.turn_off()
|
||||||
|
|
||||||
|
async def async_set_percentage(self, percentage: int) -> None:
|
||||||
|
"""Set the speed of the fan, as a percentage."""
|
||||||
|
if percentage == 0:
|
||||||
|
await self.async_turn_off()
|
||||||
|
else:
|
||||||
|
await self.cync_switch.turn_on(None,percentage*255/100,None)
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
"""Platform for light integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ColorMode, LightEntity)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
from .const import DOMAIN
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
new_devices = []
|
||||||
|
for room in hub.cync_rooms:
|
||||||
|
if not hub.cync_rooms[room]._update_callback and (room in config_entry.options["rooms"] or room in config_entry.options["subgroups"]):
|
||||||
|
new_devices.append(CyncRoomEntity(hub.cync_rooms[room]))
|
||||||
|
|
||||||
|
for switch_id in hub.cync_switches:
|
||||||
|
if not hub.cync_switches[switch_id]._update_callback and not hub.cync_switches[switch_id].plug and not hub.cync_switches[switch_id].fan and switch_id in config_entry.options["switches"]:
|
||||||
|
new_devices.append(CyncSwitchEntity(hub.cync_switches[switch_id]))
|
||||||
|
|
||||||
|
if new_devices:
|
||||||
|
async_add_entities(new_devices)
|
||||||
|
|
||||||
|
|
||||||
|
class CyncRoomEntity(LightEntity):
|
||||||
|
"""Representation of a Cync Room Light Entity."""
|
||||||
|
|
||||||
|
should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, room) -> None:
|
||||||
|
"""Initialize the light."""
|
||||||
|
self.room = room
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when this Entity has been added to HA."""
|
||||||
|
self.room.register(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Entity being removed from hass."""
|
||||||
|
self.room.reset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device registry information for this entity."""
|
||||||
|
return DeviceInfo(
|
||||||
|
identifiers = {(DOMAIN, f"{self.room.parent_room if self.room.is_subgroup else self.room.name} ({self.room.home_name})")},
|
||||||
|
manufacturer = "Cync by Savant",
|
||||||
|
name = f"{self.room.parent_room if self.room.is_subgroup else self.room.name} ({self.room.home_name})",
|
||||||
|
suggested_area = f"{self.room.parent_room if self.room.is_subgroup else self.room.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str | None:
|
||||||
|
"""Icon of the entity."""
|
||||||
|
if self.room.is_subgroup:
|
||||||
|
return "mdi:lightbulb-group-outline"
|
||||||
|
else:
|
||||||
|
return "mdi:lightbulb-group"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return Unique ID string."""
|
||||||
|
uid = 'cync_room_' + '-'.join(self.room.switches) + '_' + '-'.join(self.room.subgroups)
|
||||||
|
return uid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the room."""
|
||||||
|
return self.room.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if light is on."""
|
||||||
|
return self.room.power_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self) -> int | None:
|
||||||
|
"""Return the brightness of this room between 0..255."""
|
||||||
|
return round(self.room.brightness*255/100)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_color_temp_kelvin(self) -> int:
|
||||||
|
"""Return maximum supported color temperature."""
|
||||||
|
return self.room.max_color_temp_kelvin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_color_temp_kelvin(self) -> int:
|
||||||
|
"""Return minimum supported color temperature."""
|
||||||
|
return self.room.min_color_temp_kelvin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp_kelvin(self) -> int:
|
||||||
|
"""Return color temperature in kelvin."""
|
||||||
|
return self.room.color_temp_kelvin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rgb_color(self) -> tuple[int, int, int] | None:
|
||||||
|
"""Return the RGB color tuple of this light switch"""
|
||||||
|
return (self.room.rgb['r'],self.room.rgb['g'],self.room.rgb['b'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_color_modes(self) -> set[str] | None:
|
||||||
|
"""Return list of available color modes."""
|
||||||
|
|
||||||
|
modes: set[ColorMode | str] = set()
|
||||||
|
|
||||||
|
if self.room.support_color_temp:
|
||||||
|
modes.add(ColorMode.COLOR_TEMP)
|
||||||
|
if self.room.support_rgb:
|
||||||
|
modes.add(ColorMode.RGB)
|
||||||
|
if self.room.support_brightness:
|
||||||
|
modes.add(ColorMode.BRIGHTNESS)
|
||||||
|
if not modes:
|
||||||
|
modes.add(ColorMode.ONOFF)
|
||||||
|
|
||||||
|
return modes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_mode(self) -> str | None:
|
||||||
|
"""Return the active color mode."""
|
||||||
|
|
||||||
|
if self.room.support_color_temp:
|
||||||
|
if self.room.support_rgb and self.room.rgb['active']:
|
||||||
|
return ColorMode.RGB
|
||||||
|
else:
|
||||||
|
return ColorMode.COLOR_TEMP
|
||||||
|
if self.room.support_brightness:
|
||||||
|
return ColorMode.BRIGHTNESS
|
||||||
|
else:
|
||||||
|
return ColorMode.ONOFF
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on the light."""
|
||||||
|
await self.room.turn_on(kwargs.get(ATTR_RGB_COLOR),kwargs.get(ATTR_BRIGHTNESS),kwargs.get(ATTR_COLOR_TEMP_KELVIN))
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the light."""
|
||||||
|
await self.room.turn_off()
|
||||||
|
|
||||||
|
class CyncSwitchEntity(LightEntity):
|
||||||
|
"""Representation of a Cync Switch Light Entity."""
|
||||||
|
|
||||||
|
should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, cync_switch) -> None:
|
||||||
|
"""Initialize the light."""
|
||||||
|
self.cync_switch = cync_switch
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when this Entity has been added to HA."""
|
||||||
|
self.cync_switch.register(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Entity being removed from hass."""
|
||||||
|
self.cync_switch.reset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device registry information for this entity."""
|
||||||
|
return DeviceInfo(
|
||||||
|
identifiers = {(DOMAIN, f"{self.cync_switch.room.name} ({self.cync_switch.home_name})")},
|
||||||
|
manufacturer = "Cync by Savant",
|
||||||
|
name = f"{self.cync_switch.room.name} ({self.cync_switch.home_name})",
|
||||||
|
suggested_area = f"{self.cync_switch.room.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return Unique ID string."""
|
||||||
|
return 'cync_switch_' + self.cync_switch.device_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the switch."""
|
||||||
|
return self.cync_switch.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if light is on."""
|
||||||
|
return self.cync_switch.power_state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self) -> int | None:
|
||||||
|
"""Return the brightness of this switch between 0..255."""
|
||||||
|
return round(self.cync_switch.brightness*255/100)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_color_temp_kelvin(self) -> int:
|
||||||
|
"""Return maximum supported color temperature."""
|
||||||
|
return self.cync_switch.max_color_temp_kelvin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_color_temp_kelvin(self) -> int:
|
||||||
|
"""Return minimum supported color temperature."""
|
||||||
|
return self.cync_switch.min_color_temp_kelvin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp_kelvin(self) -> int | None:
|
||||||
|
"""Return the color temperature of this light for HA."""
|
||||||
|
return self.cync_switch.color_temp_kelvin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rgb_color(self) -> tuple[int, int, int] | None:
|
||||||
|
"""Return the RGB color tuple of this light switch"""
|
||||||
|
return (self.cync_switch.rgb['r'],self.cync_switch.rgb['g'],self.cync_switch.rgb['b'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_color_modes(self) -> set[str] | None:
|
||||||
|
"""Return list of available color modes."""
|
||||||
|
|
||||||
|
modes: set[ColorMode | str] = set()
|
||||||
|
|
||||||
|
if self.cync_switch.support_color_temp:
|
||||||
|
modes.add(ColorMode.COLOR_TEMP)
|
||||||
|
if self.cync_switch.support_rgb:
|
||||||
|
modes.add(ColorMode.RGB)
|
||||||
|
if self.cync_switch.support_brightness:
|
||||||
|
modes.add(ColorMode.BRIGHTNESS)
|
||||||
|
if not modes:
|
||||||
|
modes.add(ColorMode.ONOFF)
|
||||||
|
|
||||||
|
return modes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_mode(self) -> str | None:
|
||||||
|
"""Return the active color mode."""
|
||||||
|
|
||||||
|
if self.cync_switch.support_color_temp:
|
||||||
|
if self.cync_switch.support_rgb and self.cync_switch.rgb['active']:
|
||||||
|
return ColorMode.RGB
|
||||||
|
else:
|
||||||
|
return ColorMode.COLOR_TEMP
|
||||||
|
if self.cync_switch.support_brightness:
|
||||||
|
return ColorMode.BRIGHTNESS
|
||||||
|
else:
|
||||||
|
return ColorMode.ONOFF
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on the light."""
|
||||||
|
await self.cync_switch.turn_on(kwargs.get(ATTR_RGB_COLOR),kwargs.get(ATTR_BRIGHTNESS),kwargs.get(ATTR_COLOR_TEMP_KELVIN))
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the light."""
|
||||||
|
await self.cync_switch.turn_off()
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""Platform for light integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from .const import DOMAIN
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback
|
||||||
|
) -> None:
|
||||||
|
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
new_devices = []
|
||||||
|
for switch_id in hub.cync_switches:
|
||||||
|
if not hub.cync_switches[switch_id]._update_callback and hub.cync_switches[switch_id].plug and switch_id in config_entry.options["switches"]:
|
||||||
|
new_devices.append(CyncPlugEntity(hub.cync_switches[switch_id]))
|
||||||
|
|
||||||
|
if new_devices:
|
||||||
|
async_add_entities(new_devices)
|
||||||
|
|
||||||
|
class CyncPlugEntity(SwitchEntity):
|
||||||
|
"""Representation of a Cync Switch Light Entity."""
|
||||||
|
|
||||||
|
should_poll = False
|
||||||
|
|
||||||
|
def __init__(self, cync_switch) -> None:
|
||||||
|
"""Initialize the light."""
|
||||||
|
self.cync_switch = cync_switch
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Run when this Entity has been added to HA."""
|
||||||
|
self.cync_switch.register(self.schedule_update_ha_state)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Entity being removed from hass."""
|
||||||
|
self.cync_switch.reset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return device registry information for this entity."""
|
||||||
|
return DeviceInfo(
|
||||||
|
identifiers = {(DOMAIN, f"{self.cync_switch.room.name} ({self.cync_switch.home_name})")},
|
||||||
|
manufacturer = "Cync by Savant",
|
||||||
|
name = f"{self.cync_switch.room.name} ({self.cync_switch.home_name})",
|
||||||
|
suggested_area = f"{self.cync_switch.room.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return Unique ID string."""
|
||||||
|
return 'cync_switch_' + self.cync_switch.device_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the switch."""
|
||||||
|
return self.cync_switch.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> str | None:
|
||||||
|
"""Return the device class"""
|
||||||
|
return SwitchDeviceClass.OUTLET
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if light is on."""
|
||||||
|
return self.cync_switch.power_state
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on the outlet."""
|
||||||
|
await self.cync_switch.turn_on(None, None, None)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the outlet."""
|
||||||
|
await self.cync_switch.turn_off()
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
|||||||
|
import logging
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Configuration:
|
||||||
|
SIDEPANEL_TITLE = "sidepanel_title"
|
||||||
|
SIDEPANEL_ICON = "sidepanel_icon"
|
||||||
|
|
||||||
|
@config_entries.HANDLERS.register("dwains_dashboard")
|
||||||
|
class DwainsDashboardConfigFlow(config_entries.ConfigFlow):
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
return self.async_create_entry(title="", data={})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
return DwainsDashboardEditFlow(config_entry)
|
||||||
|
|
||||||
|
class DwainsDashboardEditFlow(config_entries.OptionsFlow):
|
||||||
|
def __init__(self, config_entry):
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
vol.Optional(SIDEPANEL_TITLE, default=self.config_entry.options.get("sidepanel_title", "Dwains Dashboard")): str,
|
||||||
|
vol.Optional(SIDEPANEL_ICON, default=self.config_entry.options.get("sidepanel_icon", "mdi:alpha-d-box")): str,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="init",
|
||||||
|
data_schema=vol.Schema(schema)
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DOMAIN = "dwains_dashboard"
|
||||||
|
VERSION = "3.8.0"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
src/*
|
||||||
|
!src/translations.js
|
||||||
|
dwains-dashboard.js.map
|
||||||
+6172
File diff suppressed because one or more lines are too long
+1713
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user