├── .gitattributes ├── requirements_dev.txt ├── tests └── data │ ├── test.json │ ├── brightness_set.json │ ├── monitor_on.json │ ├── monitor_off.json │ ├── monitor_status.json │ ├── no_api_key.json │ ├── brightness_get.json │ ├── update.json │ ├── config.json │ └── module.json ├── custom_components ├── __init__.py └── magicmirror │ ├── manifest.json │ ├── const.py │ ├── translations │ └── en.json │ ├── strings.json │ ├── notify.py │ ├── diagnostics.py │ ├── config_flow.py │ ├── __init__.py │ ├── coordinator.py │ ├── button.py │ ├── light.py │ ├── switch.py │ ├── models.py │ ├── update.py │ └── api.py ├── requirements_test.txt ├── requirements.txt ├── hacs.json ├── scripts ├── lint ├── setup └── develop ├── .gitignore ├── .vscode └── tasks.json ├── config └── configuration.yaml ├── .github ├── workflows │ ├── cron.yml │ ├── push.yml │ └── pull.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── issue.md ├── .ruff.toml ├── setup.cfg ├── .devcontainer.json ├── README.md ├── CONTRIBUTING.md └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | homeassistant 2 | -------------------------------------------------------------------------------- /tests/data/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true 3 | } 4 | -------------------------------------------------------------------------------- /custom_components/__init__.py: -------------------------------------------------------------------------------- 1 | """Custom components module.""" 2 | -------------------------------------------------------------------------------- /tests/data/brightness_set.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true 3 | } 4 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest-homeassistant-custom-component==0.4.3 2 | -------------------------------------------------------------------------------- /tests/data/monitor_on.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "monitor": "on" 4 | } 5 | -------------------------------------------------------------------------------- /tests/data/monitor_off.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "monitor": "off" 4 | } 5 | -------------------------------------------------------------------------------- /tests/data/monitor_status.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "monitor": "on" 4 | } 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorlog==6.9.0 2 | homeassistant==2024.12.0 3 | pip>=21.3.1 4 | ruff==0.7.2 5 | numpy -------------------------------------------------------------------------------- /tests/data/no_api_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": false, 3 | "message": "Forbidden: API Key Not Provided!" 4 | } 5 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Magic Mirror", 3 | "homeassistant": "2022.4.0", 4 | "render_readme": true 5 | } 6 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | ruff format . 8 | ruff check . --fix -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | pythonenv* 3 | venv 4 | .venv 5 | .coverage 6 | .idea 7 | 8 | config/* 9 | !config/configuration.yaml 10 | -------------------------------------------------------------------------------- /tests/data/brightness_get.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "query": { 4 | "data": "brightness" 5 | }, 6 | "result": 30 7 | } 8 | -------------------------------------------------------------------------------- /tests/data/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "query": { 4 | "data": "mmUpdateAvailable" 5 | }, 6 | "result": false 7 | } 8 | -------------------------------------------------------------------------------- /scripts/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | python3 -m pip install --requirement requirements.txt -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run Home Assistant on port 8123", 6 | "type": "shell", 7 | "command": "scripts/develop", 8 | "problemMatcher": [] 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /custom_components/magicmirror/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "magicmirror", 3 | "name": "Magic Mirror", 4 | "config_flow": true, 5 | "documentation": "https://www.github.com/sindrebroch/ha-magicmirror", 6 | "issue_tracker": "https://github.com/sindrebroch/ha-magicmirror/issues", 7 | "requirements": [], 8 | "codeowners": [ 9 | "@sindrebroch" 10 | ], 11 | "iot_class": "local_polling", 12 | "version": "1.3.0" 13 | } -------------------------------------------------------------------------------- /custom_components/magicmirror/const.py: -------------------------------------------------------------------------------- 1 | """Constants for MagicMirror.""" 2 | 3 | from logging import Logger, getLogger 4 | 5 | from homeassistant.const import Platform 6 | 7 | LOGGER: Logger = getLogger(__package__) 8 | 9 | DOMAIN = "magicmirror" 10 | PLATFORMS = [ 11 | Platform.BUTTON, 12 | Platform.LIGHT, 13 | Platform.SWITCH, 14 | Platform.UPDATE, 15 | ] 16 | DATA_HASS_CONFIG = "mm_hass_config" 17 | ATTR_CONFIG_ENTRY_ID = "entry_id" 18 | -------------------------------------------------------------------------------- /config/configuration.yaml: -------------------------------------------------------------------------------- 1 | # https://www.home-assistant.io/integrations/default_config/ 2 | # default_config: 3 | 4 | config: 5 | #dhcp: 6 | history: 7 | homeassistant_alerts: 8 | logbook: 9 | ssdp: 10 | sun: 11 | #webhook: 12 | zeroconf: 13 | 14 | # https://www.home-assistant.io/integrations/homeassistant/ 15 | homeassistant: 16 | debug: true 17 | 18 | # https://www.home-assistant.io/integrations/logger/ 19 | logger: 20 | default: info 21 | logs: 22 | custom_components.magicmirror: debug 23 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Cron actions 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | validate: 9 | runs-on: "ubuntu-latest" 10 | name: Validate 11 | steps: 12 | - uses: "actions/checkout@v2" 13 | 14 | - name: HACS validation 15 | uses: "hacs/action@main" 16 | with: 17 | category: "integration" 18 | ignore: brands 19 | 20 | - name: Hassfest validation 21 | uses: "home-assistant/actions/hassfest@master" 22 | -------------------------------------------------------------------------------- /custom_components/magicmirror/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect", 8 | "invalid_auth": "Invalid authentication", 9 | "unknown": "Unexpected error" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "host": "Host", 15 | "port": "Port", 16 | "api_key": "API-key" 17 | } 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /scripts/develop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | # Create config dir if not present 8 | if [[ ! -d "${PWD}/config" ]]; then 9 | mkdir -p "${PWD}/config" 10 | hass --config "${PWD}/config" --script ensure_config 11 | fi 12 | 13 | # Set the path to custom_components 14 | ## This let's us have the structure we want /custom_components/integration_blueprint 15 | ## while at the same time have Home Assistant configuration inside /config 16 | ## without resulting to symlinks. 17 | export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" 18 | 19 | # Start Home Assistant 20 | hass --config "${PWD}/config" --debug -------------------------------------------------------------------------------- /custom_components/magicmirror/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "host": "[%key:common::config_flow::data::host%]", 7 | "port": "[%key:common::config_flow::data::port%]", 8 | "api_key": "[%key:common::config_flow::data::api_key%]" 9 | } 10 | } 11 | }, 12 | "error": { 13 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 14 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 15 | "unknown": "[%key:common::config_flow::error::unknown%]" 16 | }, 17 | "abort": { 18 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.ruff.toml: -------------------------------------------------------------------------------- 1 | # The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml 2 | 3 | target-version = "py312" 4 | 5 | [lint] 6 | select = [ 7 | "ALL", 8 | ] 9 | 10 | ignore = [ 11 | "ANN101", # Missing type annotation for `self` in method 12 | "ANN401", # Dynamically typed expressions (typing.Any) are disallowed 13 | "D203", # no-blank-line-before-class (incompatible with formatter) 14 | "D212", # multi-line-summary-first-line (incompatible with formatter) 15 | "COM812", # incompatible with formatter 16 | "ISC001", # incompatible with formatter 17 | ] 18 | 19 | [lint.flake8-pytest-style] 20 | fixture-parentheses = false 21 | 22 | [lint.pyupgrade] 23 | keep-runtime-typing = true 24 | 25 | [lint.mccabe] 26 | max-complexity = 25 -------------------------------------------------------------------------------- /tests/data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "query": { 4 | "data": "config" 5 | }, 6 | "data": { 7 | "address": "0.0.0.0", 8 | "port": 8080, 9 | "basePath": "/", 10 | "kioskmode": false, 11 | "electronOptions": {}, 12 | "ipWhitelist": [], 13 | "language": "en", 14 | "logLevel": [ 15 | "INFO", 16 | "LOG", 17 | "WARN", 18 | "ERROR" 19 | ], 20 | "timeFormat": 24, 21 | "units": "metric", 22 | "zoom": 1, 23 | "customCss": "css/custom.css", 24 | "modules": [{}], 25 | "paths": { 26 | "modules": "modules", 27 | "vendor": "vendor" 28 | }, 29 | "useHttps": false, 30 | "httpsPrivateKey": "", 31 | "httpsCertificate": "", 32 | "locale": "" 33 | } 34 | } -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push actions 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | name: Validate 13 | steps: 14 | - uses: "actions/checkout@v2" 15 | 16 | - name: HACS validation 17 | uses: "hacs/action@main" 18 | with: 19 | category: "integration" 20 | ignore: brands 21 | 22 | - name: Hassfest validation 23 | uses: "home-assistant/actions/hassfest@master" 24 | 25 | style: 26 | runs-on: "ubuntu-latest" 27 | name: Check style formatting 28 | steps: 29 | - uses: "actions/checkout@v2" 30 | - uses: "actions/setup-python@v1" 31 | with: 32 | python-version: "3.x" 33 | - run: python3 -m pip install black 34 | - run: black . 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | 16 | 17 | ## Version of the custom_component 18 | 21 | 22 | ## Configuration 23 | 24 | ```yaml 25 | 26 | Add your logs here. 27 | 28 | ``` 29 | 30 | ## Describe the bug 31 | A clear and concise description of what the bug is. 32 | 33 | 34 | ## Debug log 35 | 36 | 37 | 38 | ```text 39 | 40 | Add your logs here. 41 | 42 | ``` -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build 3 | doctests = True 4 | # To work with Black 5 | max-line-length = 88 6 | # E501: line too long 7 | # W503: Line break occurred before a binary operator 8 | # E203: Whitespace before ':' 9 | # D202 No blank lines allowed after function docstring 10 | # W504 line break after binary operator 11 | ignore = 12 | E501, 13 | W503, 14 | E203, 15 | D202, 16 | W504 17 | 18 | [isort] 19 | # https://github.com/timothycrosley/isort 20 | # https://github.com/timothycrosley/isort/wiki/isort-Settings 21 | # splits long import on multiple lines indented by 4 spaces 22 | multi_line_output = 3 23 | include_trailing_comma=True 24 | force_grid_wrap=0 25 | use_parentheses=True 26 | line_length=88 27 | indent = " " 28 | # by default isort don't check module indexes 29 | not_skip = __init__.py 30 | # will group `import x` and `from x import` of the same module. 31 | force_sort_within_sections = true 32 | sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 33 | default_section = THIRDPARTY 34 | known_first_party = custom_components.integration_blueprint, tests 35 | combine_as_imports = true 36 | -------------------------------------------------------------------------------- /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ludeeus/integration_blueprint", 3 | "image": "mcr.microsoft.com/devcontainers/python:dev-3.13", 4 | "postCreateCommand": "scripts/setup", 5 | "forwardPorts": [ 6 | 8123 7 | ], 8 | "portsAttributes": { 9 | "8123": { 10 | "label": "Home Assistant", 11 | "onAutoForward": "notify" 12 | } 13 | }, 14 | "customizations": { 15 | "vscode": { 16 | "extensions": [ 17 | "charliermarsh.ruff", 18 | "github.vscode-pull-request-github", 19 | "ms-python.python", 20 | "ms-python.vscode-pylance", 21 | "ryanluker.vscode-coverage-gutters" 22 | ], 23 | "settings": { 24 | "files.eol": "\n", 25 | "editor.tabSize": 4, 26 | "editor.formatOnPaste": true, 27 | "editor.formatOnSave": true, 28 | "editor.formatOnType": false, 29 | "files.trimTrailingWhitespace": true, 30 | "python.analysis.typeCheckingMode": "basic", 31 | "python.analysis.autoImportCompletions": true, 32 | "python.defaultInterpreterPath": "/usr/local/bin/python", 33 | "[python]": { 34 | "editor.defaultFormatter": "charliermarsh.ruff" 35 | } 36 | } 37 | } 38 | }, 39 | "remoteUser": "vscode", 40 | "features": {} 41 | } -------------------------------------------------------------------------------- /.github/workflows/pull.yml: -------------------------------------------------------------------------------- 1 | name: Pull actions 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | validate: 8 | runs-on: "ubuntu-latest" 9 | name: Validate 10 | steps: 11 | - uses: "actions/checkout@v2" 12 | 13 | - name: HACS validation 14 | uses: "hacs/action@main" 15 | with: 16 | category: "integration" 17 | ignore: brands 18 | 19 | - name: Hassfest validation 20 | uses: "home-assistant/actions/hassfest@master" 21 | 22 | style: 23 | runs-on: "ubuntu-latest" 24 | name: Check style formatting 25 | steps: 26 | - uses: "actions/checkout@v2" 27 | - uses: "actions/setup-python@v1" 28 | with: 29 | python-version: "3.x" 30 | - run: python3 -m pip install black 31 | - run: black . 32 | 33 | tests: 34 | runs-on: "ubuntu-latest" 35 | name: Run tests 36 | steps: 37 | - name: Check out code from GitHub 38 | uses: "actions/checkout@v2" 39 | - name: Setup Python 40 | uses: "actions/setup-python@v1" 41 | with: 42 | python-version: "3.8" 43 | - name: Install requirements 44 | run: python3 -m pip install -r requirements_test.txt 45 | - name: Run tests 46 | run: | 47 | pytest \ 48 | -qq \ 49 | --timeout=9 \ 50 | --durations=10 \ 51 | -n auto \ 52 | --cov custom_components.integration_blueprint \ 53 | -o console_output_style=count \ 54 | -p no:sugar \ 55 | tests 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MagicMirror for HomeAssistant 2 | 3 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/sindrebroch/ha-magicmirror?style=flat-square) 4 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) 5 | 6 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/sindrebroch) 7 | 8 | Requires [Jopyth/MMM-Remote-Control](https://github.com/Jopyth/MMM-Remote-Control) installed on your MagicMirror host, with an API-key configured. 9 | 10 | ## Installation 11 | 12 |
13 | HACS 14 | 15 | 1. Ensure that [HACS](https://hacs.xyz/) is installed. 16 | 2. Add this repository as a custom repository 17 | 3. Search for and install the "MagicMirror"-integration. 18 | 4. Restart Home Assistant. 19 | 5. Configure the `MagicMirror` integration. 20 |
21 | 22 | ## Features 23 | ### Light 24 | - Toggle monitor on/off 25 | - Change brightness 26 | 27 | ### Switch 28 | - Show / hide modules (See [Note](https://github.com/sindrebroch/ha-magicmirror#note)) 29 | 30 | ### Button 31 | - Shutdown host 32 | - Reboot host 33 | - Restart magicmirror 34 | - Refresh browser 35 | 36 | ### Update 37 | - MagicMirror update 38 | - Module update (supports installing new version) 39 | 40 | ### Notify 41 | ``` 42 | service: notify.magicmirror 43 | data: 44 | title: Title # optional 45 | message: Message # required 46 | data: 47 | timer: 5000 # default, optional 48 | dropdown: False # default, optional 49 | ``` 50 | 51 | ## Note 52 | Module controls are using an ID from the API which is generated from MagicMirror config.js. This means that if you change the order of your config.js, the modules might become out of sync. This **_should_** be fixed by reloading the integration, to have new devices generated. The old ones needs to be deleted. 53 | -------------------------------------------------------------------------------- /custom_components/magicmirror/notify.py: -------------------------------------------------------------------------------- 1 | """Support for MagicMirror notifications.""" 2 | 3 | import asyncio 4 | import logging 5 | from typing import Any 6 | 7 | import voluptuous as vol 8 | from homeassistant.components.notify import PLATFORM_SCHEMA 9 | from homeassistant.components.notify.const import ATTR_TITLE 10 | from homeassistant.components.notify.legacy import BaseNotificationService 11 | 12 | from custom_components.magicmirror.const import ATTR_CONFIG_ENTRY_ID, DOMAIN 13 | from custom_components.magicmirror.coordinator import MagicMirrorDataUpdateCoordinator 14 | 15 | _LOGGER = logging.getLogger(__name__) 16 | 17 | CONF_TIMER = "timer" 18 | CONF_DROPDOWN = "dropdown" 19 | 20 | DEFAULT_TIMER = 5000 21 | DEFAULT_DROPDOWN = False 22 | 23 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 24 | {vol.Required(CONF_TIMER): vol.Coerce(int), vol.Required(CONF_DROPDOWN): str} 25 | ) 26 | 27 | 28 | async def async_get_service(hass, _, discovery_info=None): 29 | """Get the MagicMirror notification service.""" 30 | 31 | entry_id = discovery_info[ATTR_CONFIG_ENTRY_ID] 32 | coordinator: MagicMirrorDataUpdateCoordinator = hass.data[DOMAIN][entry_id] 33 | return MagicMirrorNotificationService(coordinator.api) 34 | 35 | 36 | class MagicMirrorNotificationService(BaseNotificationService): 37 | """Implement the notification service for MagicMirror.""" 38 | 39 | def __init__(self, notify): 40 | """Initialize the service.""" 41 | self._notify = notify 42 | 43 | async def async_send_message(self, message: str, **kwargs: Any) -> None: 44 | """Send a message to MagicMirror devices.""" 45 | 46 | title = kwargs.get(ATTR_TITLE, "") 47 | 48 | data = kwargs["data"] 49 | 50 | if data is None: 51 | timer = DEFAULT_TIMER 52 | alert_type = DEFAULT_DROPDOWN 53 | else: 54 | timer = data.get(CONF_TIMER, DEFAULT_TIMER) 55 | alert_type = data.get(CONF_DROPDOWN, DEFAULT_DROPDOWN) 56 | 57 | try: 58 | await self._notify.alert( 59 | title=title, 60 | msg=message, 61 | timer=timer, 62 | dropdown=bool(alert_type), 63 | ) 64 | except asyncio.TimeoutError: 65 | _LOGGER.error("Timeout sending message with MagicMirror") 66 | -------------------------------------------------------------------------------- /custom_components/magicmirror/diagnostics.py: -------------------------------------------------------------------------------- 1 | """Diagnostics support for MagicMirror.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.core import HomeAssistant 7 | from homeassistant.helpers.device_registry import DeviceEntry 8 | 9 | from custom_components.magicmirror.api import MagicMirrorApiClient 10 | from custom_components.magicmirror.const import DOMAIN, LOGGER 11 | from custom_components.magicmirror.coordinator import MagicMirrorDataUpdateCoordinator 12 | 13 | 14 | async def async_get_config_entry_diagnostics( 15 | hass: HomeAssistant, entry: ConfigEntry 16 | ) -> dict[str, Any]: 17 | """Return diagnostics for a config entry.""" 18 | LOGGER.debug("diagnostics entry %s", entry.as_dict()) 19 | 20 | coordinator: MagicMirrorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] 21 | api: MagicMirrorApiClient = coordinator.api 22 | data = coordinator.data 23 | LOGGER.debug("diagnostics data %s", data) 24 | 25 | return { 26 | "host": api.host, 27 | "port": api.port, 28 | "brightness": data.brightness, 29 | "monitor_status": data.monitor_status, 30 | "update_available": data.update_available, 31 | "module_updates": data.module_updates, 32 | "modules": str(data.modules), 33 | } 34 | 35 | # todo 36 | # TO_REDACT = [ 37 | # CONF_API_KEY, 38 | # APPLIANCE_CODE 39 | # ] 40 | # return { 41 | # "entry_data": async_redact_data(entry.data, TO_REDACT), 42 | # "data": entry.runtime_data.data, 43 | # } 44 | 45 | 46 | async def async_get_device_diagnostics( 47 | hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry 48 | ) -> dict[str, Any]: 49 | """Return diagnostics for a device.""" 50 | LOGGER.debug("diagnostics device %s", device) 51 | LOGGER.debug("diagnostics entry %s", entry.as_dict()) 52 | coordinator: MagicMirrorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] 53 | api: MagicMirrorApiClient = coordinator.api 54 | data = coordinator.data 55 | return { 56 | "host": api.host, 57 | "port": api.port, 58 | "brightness": data.brightness, 59 | "monitor_status": data.monitor_status, 60 | "update_available": data.update_available, 61 | "module_updates": data.module_updates, 62 | "modules": str(data.modules), 63 | } 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | 3 | Contributing to this project should be as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | 10 | ## Github is used for everything 11 | 12 | Github is used to host code, to track issues and feature requests, as well as accept pull requests. 13 | 14 | Pull requests are the best way to propose changes to the codebase. 15 | 16 | 1. Fork the repo and create your branch from `master`. 17 | 2. If you've changed something, update the documentation. 18 | 3. Make sure your code lints (using black). 19 | 4. Test you contribution. 20 | 5. Issue that pull request! 21 | 22 | ## Any contributions you make will be under the MIT Software License 23 | 24 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 25 | 26 | ## Report bugs using Github's [issues](../../issues) 27 | 28 | GitHub issues are used to track public bugs. 29 | Report a bug by [opening a new issue](../../issues/new/choose); it's that easy! 30 | 31 | ## Write bug reports with detail, background, and sample code 32 | 33 | **Great Bug Reports** tend to have: 34 | 35 | - A quick summary and/or background 36 | - Steps to reproduce 37 | - Be specific! 38 | - Give sample code if you can. 39 | - What you expected would happen 40 | - What actually happens 41 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 42 | 43 | People *love* thorough bug reports. I'm not even kidding. 44 | 45 | ## Use a Consistent Coding Style 46 | 47 | Use [black](https://github.com/ambv/black) to make sure the code follows the style. 48 | 49 | ## Test your code modification 50 | 51 | This custom component is based on [integration_blueprint template](https://github.com/custom-components/integration_blueprint). 52 | 53 | It comes with development environment in a container, easy to launch 54 | if you use Visual Studio Code. With this container you will have a stand alone 55 | Home Assistant instance running and already configured with the included 56 | [`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml) 57 | file. 58 | 59 | ## License 60 | 61 | By contributing, you agree that your contributions will be licensed under its MIT License. 62 | -------------------------------------------------------------------------------- /tests/data/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "data": [ 4 | { 5 | "index": 0, 6 | "identifier": "module_0_alert", 7 | "name": "alert", 8 | "path": "modules/default/alert/", 9 | "file": "alert.js", 10 | "configDeepMerge": false, 11 | "config": { 12 | "effect": "slide", 13 | "alert_effect": "jelly", 14 | "display_time": 3500, 15 | "position": "center", 16 | "welcome_message": false 17 | }, 18 | "classes": "alert", 19 | "hidden": false, 20 | "lockStrings": [], 21 | "actions": { 22 | "showalert": { 23 | "notification": "SHOW_ALERT", 24 | "guessed": true 25 | }, 26 | "hidealert": { 27 | "notification": "HIDE_ALERT", 28 | "guessed": true 29 | } 30 | } 31 | }, 32 | { 33 | "index": 1, 34 | "identifier": "module_1_updatenotification", 35 | "name": "updatenotification", 36 | "path": "modules/default/updatenotification/", 37 | "file": "updatenotification.js", 38 | "position": "top_bar", 39 | "configDeepMerge": false, 40 | "config": { 41 | "updateInterval": 600000, 42 | "refreshInterval": 86400000, 43 | "ignoreModules": [], 44 | "timeout": 5000 45 | }, 46 | "classes": "updatenotification", 47 | "hidden": false, 48 | "lockStrings": [] 49 | }, 50 | { 51 | "index": 2, 52 | "identifier": "module_2_clock", 53 | "name": "clock", 54 | "path": "modules/default/clock/", 55 | "file": "clock.js", 56 | "position": "top_left", 57 | "configDeepMerge": false, 58 | "config": { 59 | "displayType": "digital", 60 | "timeFormat": 24, 61 | "displaySeconds": true, 62 | "showPeriod": true, 63 | "showPeriodUpper": false, 64 | "clockBold": false, 65 | "showDate": true, 66 | "showWeek": false, 67 | "dateFormat": "dddd, LL", 68 | "analogSize": "200px", 69 | "analogFace": "simple", 70 | "analogPlacement": "bottom", 71 | "analogShowDate": "top", 72 | "secondsColor": "#888888", 73 | "timezone": null, 74 | "showSunTimes": false, 75 | "showMoonTimes": false, 76 | "lat": 0, 77 | "lon": 0 78 | }, 79 | "classes": "clock", 80 | "hidden": false, 81 | "lockStrings": [] 82 | } 83 | ] 84 | } -------------------------------------------------------------------------------- /custom_components/magicmirror/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for MagicMirror.""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing import Any 6 | 7 | import aiohttp 8 | import voluptuous as vol 9 | from homeassistant import config_entries 10 | from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_NAME, CONF_PORT 11 | from homeassistant.data_entry_flow import FlowResult 12 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 13 | 14 | from custom_components.magicmirror.api import MagicMirrorApiClient 15 | from custom_components.magicmirror.const import DOMAIN, LOGGER 16 | from custom_components.magicmirror.models import GenericResponse 17 | 18 | SCHEMA = vol.Schema( 19 | { 20 | vol.Required(CONF_NAME, default="MagicMirror"): str, 21 | vol.Required(CONF_HOST): str, 22 | vol.Required(CONF_PORT, default="8080"): str, 23 | vol.Required(CONF_API_KEY): str, 24 | } 25 | ) 26 | 27 | 28 | class MagicMirrorFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 29 | """Config flow for MagicMirror.""" 30 | 31 | VERSION = 1 32 | 33 | async def async_step_user( 34 | self, user_input: dict[str, Any] | None = None 35 | ) -> FlowResult: 36 | """Handle a flow initialized by the user.""" 37 | if user_input is not None: 38 | name = user_input[CONF_NAME] 39 | host = user_input[CONF_HOST] 40 | port = user_input[CONF_PORT] 41 | api_key = user_input[CONF_API_KEY] 42 | 43 | if await self._async_existing_devices(host): 44 | return self.async_abort(reason="already_configured") 45 | 46 | api = MagicMirrorApiClient( 47 | name, host, port, api_key, session=async_get_clientsession(self.hass) 48 | ) 49 | 50 | errors: dict[str, Any] = {} 51 | 52 | try: 53 | response: GenericResponse = await api.api_test() 54 | 55 | if not response.success: 56 | errors["base"] = "cannot_connect" 57 | 58 | except aiohttp.ClientError as error: 59 | errors["base"] = "cannot_connect" 60 | LOGGER.warning("error=%s. errors=%s", error, errors) 61 | 62 | if errors: 63 | return self.async_show_form( 64 | step_id="user", data_schema=SCHEMA, errors=errors 65 | ) 66 | 67 | unique_id: str = host 68 | await self.async_set_unique_id(unique_id) 69 | self._abort_if_unique_id_configured() 70 | 71 | return self.async_create_entry( 72 | title=unique_id.title(), 73 | data=user_input, 74 | ) 75 | 76 | return self.async_show_form( 77 | step_id="user", 78 | data_schema=SCHEMA, 79 | errors={}, 80 | ) 81 | 82 | async def _async_existing_devices(self, host: str) -> bool: 83 | """Find existing devices.""" 84 | existing_devices = [ 85 | f"{entry.data.get(CONF_HOST)}" for entry in self._async_current_entries() 86 | ] 87 | return host in existing_devices 88 | -------------------------------------------------------------------------------- /custom_components/magicmirror/__init__.py: -------------------------------------------------------------------------------- 1 | """The MagicMirror integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.const import ( 7 | CONF_API_KEY, 8 | CONF_HOST, 9 | CONF_NAME, 10 | CONF_PORT, 11 | Platform, 12 | ) 13 | from homeassistant.core import HomeAssistant 14 | from homeassistant.helpers import device_registry as dr 15 | from homeassistant.helpers import discovery 16 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 17 | from homeassistant.helpers.typing import ConfigType 18 | 19 | from custom_components.magicmirror.api import MagicMirrorApiClient 20 | from custom_components.magicmirror.const import ( 21 | ATTR_CONFIG_ENTRY_ID, 22 | DATA_HASS_CONFIG, 23 | DOMAIN, 24 | PLATFORMS, 25 | ) 26 | from custom_components.magicmirror.coordinator import MagicMirrorDataUpdateCoordinator 27 | 28 | 29 | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 30 | """Set up the MagicMirror component.""" 31 | hass.data[DATA_HASS_CONFIG] = config 32 | return True 33 | 34 | 35 | async def async_remove_config_entry_device( 36 | hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry 37 | ) -> bool: 38 | """Remove config entry device.""" 39 | return True 40 | 41 | 42 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 43 | """Set up MagicMirror from a config entry.""" 44 | hass.data.setdefault(DOMAIN, {}) 45 | 46 | api = MagicMirrorApiClient( 47 | host=entry.data[CONF_HOST], 48 | port=entry.data[CONF_PORT], 49 | api_key=entry.data[CONF_API_KEY], 50 | session=async_get_clientsession(hass), 51 | ) 52 | 53 | name = entry.data.get(CONF_NAME, "MagicMirror") 54 | coordinator = MagicMirrorDataUpdateCoordinator(hass, api, name) 55 | 56 | await coordinator.async_config_entry_first_refresh() 57 | 58 | hass.data[DOMAIN][entry.entry_id] = coordinator 59 | 60 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 61 | 62 | await async_setup_notify(hass, entry) 63 | 64 | return True 65 | 66 | 67 | async def async_setup_notify(hass: HomeAssistant, entry: ConfigEntry) -> bool: 68 | """Set up notification platform.""" 69 | hass.async_create_task( 70 | discovery.async_load_platform( 71 | hass, 72 | Platform.NOTIFY, 73 | DOMAIN, 74 | { 75 | CONF_NAME: DOMAIN, 76 | ATTR_CONFIG_ENTRY_ID: entry.entry_id, 77 | }, 78 | hass.data[DATA_HASS_CONFIG], 79 | ) 80 | ) 81 | return True 82 | 83 | 84 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 85 | """Unload a config entry.""" 86 | unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 87 | 88 | if unload_ok: 89 | hass.data[DOMAIN].pop(entry.entry_id) 90 | 91 | return unload_ok 92 | 93 | 94 | async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: 95 | """Reload config entry.""" 96 | await async_unload_entry(hass, entry) 97 | await async_setup_entry(hass, entry) 98 | -------------------------------------------------------------------------------- /custom_components/magicmirror/coordinator.py: -------------------------------------------------------------------------------- 1 | """The MagicMirror integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | from datetime import timedelta 6 | 7 | from aiohttp.client_exceptions import ClientConnectorError 8 | from async_timeout import timeout 9 | from homeassistant.core import HomeAssistant 10 | from homeassistant.helpers.device_registry import DeviceInfo 11 | from homeassistant.helpers.update_coordinator import ( 12 | DataUpdateCoordinator, 13 | UpdateFailed, 14 | ) 15 | from voluptuous.error import Error 16 | 17 | from custom_components.magicmirror.api import MagicMirrorApiClient 18 | from custom_components.magicmirror.const import DOMAIN, LOGGER 19 | from custom_components.magicmirror.models import ( 20 | MagicMirrorData, 21 | ModuleResponse, 22 | ModuleUpdateResponses, 23 | MonitorResponse, 24 | QueryResponse, 25 | ) 26 | 27 | 28 | class MagicMirrorDataUpdateCoordinator(DataUpdateCoordinator): 29 | """Class to manage fetching MagicMirror data.""" 30 | 31 | data: MagicMirrorData 32 | 33 | def __init__( 34 | self, hass: HomeAssistant, api: MagicMirrorApiClient, name: str 35 | ) -> None: 36 | """Initialize.""" 37 | self.api = api 38 | self.name = name 39 | 40 | self._attr_device_info = DeviceInfo( 41 | name=name, 42 | model="MagicMirror", 43 | manufacturer="MagicMirror", 44 | identifiers={(DOMAIN, "MagicMirror")}, 45 | configuration_url=f"{api.base_url}/remote.html", 46 | ) 47 | 48 | super().__init__( 49 | hass, 50 | LOGGER, 51 | name=DOMAIN, 52 | update_interval=timedelta(minutes=1), 53 | ) 54 | 55 | async def _async_update_data(self) -> MagicMirrorData: 56 | """Update data via library.""" 57 | try: 58 | async with timeout(20): 59 | update: QueryResponse = await self.api.mm_update_available() 60 | module_updates: ModuleUpdateResponses = ( 61 | await self.api.update_available() 62 | ) 63 | monitor: MonitorResponse = await self.api.monitor_status() 64 | brightness: QueryResponse = await self.api.get_brightness() 65 | modules: ModuleResponse = await self.api.get_modules() 66 | 67 | if not monitor.success: 68 | LOGGER.warning("Failed to fetch monitor-status for MagicMirror") 69 | if not update.success: 70 | LOGGER.warning("Failed to fetch update-status for MagicMirror") 71 | if not brightness.success: 72 | LOGGER.warning("Failed to fetch brightness for MagicMirror") 73 | if not modules.success: 74 | LOGGER.warning("Failed to fetch modules for MagicMirror") 75 | if not module_updates.success: 76 | LOGGER.warning("Failed to fetch module updates for MagicMirror") 77 | 78 | return MagicMirrorData( 79 | monitor_status=monitor.monitor, 80 | update_available=update.result, 81 | module_updates=module_updates.result, 82 | brightness=int(brightness.result), 83 | modules=modules.data, 84 | ) 85 | 86 | except (Error, ClientConnectorError) as error: 87 | LOGGER.error("Update error %s", error) 88 | raise UpdateFailed(error) from error 89 | -------------------------------------------------------------------------------- /custom_components/magicmirror/button.py: -------------------------------------------------------------------------------- 1 | """Button for MagicMirror.""" 2 | 3 | from homeassistant.components.button import ( 4 | ButtonDeviceClass, 5 | ButtonEntity, 6 | ButtonEntityDescription, 7 | ) 8 | from homeassistant.config_entries import ConfigEntry 9 | from homeassistant.core import HomeAssistant 10 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 11 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 12 | 13 | from custom_components.magicmirror.const import DOMAIN 14 | from custom_components.magicmirror.coordinator import MagicMirrorDataUpdateCoordinator 15 | from custom_components.magicmirror.models import Entity 16 | 17 | 18 | async def async_setup_entry( 19 | hass: HomeAssistant, 20 | entry: ConfigEntry, 21 | async_add_entities: AddEntitiesCallback, 22 | ) -> None: 23 | """Add MagicMirror entities from a config_entry.""" 24 | coordinator: MagicMirrorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] 25 | 26 | async_add_entities( 27 | [ 28 | MagicMirrorShutdownButton( 29 | coordinator, 30 | ButtonEntityDescription( 31 | key=Entity.SHUTDOWN.value, 32 | name="MagicMirror Shutdown Host", 33 | icon="mdi:power", 34 | ), 35 | ), 36 | MagicMirrorRestartButton( 37 | coordinator, 38 | ButtonEntityDescription( 39 | key=Entity.RESTART.value, 40 | name="MagicMirror Restart MagicMirror", 41 | icon="mdi:restart", 42 | device_class=ButtonDeviceClass.RESTART, 43 | ), 44 | ), 45 | MagicMirrorRebootButton( 46 | coordinator, 47 | ButtonEntityDescription( 48 | key=Entity.REBOOT.value, 49 | name="MagicMirror Reboot Host", 50 | icon="mdi:restart", 51 | device_class=ButtonDeviceClass.RESTART, 52 | ), 53 | ), 54 | MagicMirrorRefreshButton( 55 | coordinator, 56 | ButtonEntityDescription( 57 | key=Entity.REFRESH.value, 58 | name="MagicMirror Refresh Browser", 59 | icon="mdi:refresh", 60 | ), 61 | ), 62 | ] 63 | ) 64 | 65 | 66 | class MagicMirrorButton(CoordinatorEntity, ButtonEntity): 67 | """Define a MagicMirror entity.""" 68 | 69 | coordinator: MagicMirrorDataUpdateCoordinator 70 | 71 | def __init__( 72 | self, 73 | coordinator: MagicMirrorDataUpdateCoordinator, 74 | description: ButtonEntityDescription, 75 | ) -> None: 76 | """Initialize.""" 77 | super().__init__(coordinator) 78 | self.coordinator = coordinator 79 | self.entity_description = description 80 | 81 | self._attr_unique_id = f"{description.key}" 82 | self._attr_device_info = self.coordinator._attr_device_info 83 | 84 | async def async_press(self) -> None: 85 | """Handle the button press.""" 86 | 87 | 88 | class MagicMirrorShutdownButton(MagicMirrorButton): 89 | """Shutdown button.""" 90 | 91 | async def async_press(self) -> None: 92 | """Shut down magicmirror.""" 93 | await self.coordinator.api.shutdown() 94 | 95 | 96 | class MagicMirrorRestartButton(MagicMirrorButton): 97 | """Restart button.""" 98 | 99 | async def async_press(self) -> None: 100 | """Restart magicmirror.""" 101 | await self.coordinator.api.restart() 102 | 103 | 104 | class MagicMirrorRebootButton(MagicMirrorButton): 105 | """Reboot button.""" 106 | 107 | async def async_press(self) -> None: 108 | """Reboot magicmirror.""" 109 | await self.coordinator.api.reboot() 110 | 111 | 112 | class MagicMirrorRefreshButton(MagicMirrorButton): 113 | """Refresh button.""" 114 | 115 | async def async_press(self) -> None: 116 | """Refresh magicmirror.""" 117 | await self.coordinator.api.refresh() 118 | -------------------------------------------------------------------------------- /custom_components/magicmirror/light.py: -------------------------------------------------------------------------------- 1 | """Light entity for MagicMirror.""" 2 | 3 | from math import ceil 4 | from typing import Any 5 | 6 | from homeassistant.components.light import ( 7 | ATTR_BRIGHTNESS, 8 | ColorMode, 9 | LightEntity, 10 | LightEntityDescription, 11 | ) 12 | from homeassistant.config_entries import ConfigEntry 13 | from homeassistant.const import STATE_ON 14 | from homeassistant.core import HomeAssistant, callback 15 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 16 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 17 | 18 | from custom_components.magicmirror.const import DOMAIN 19 | from custom_components.magicmirror.coordinator import MagicMirrorDataUpdateCoordinator 20 | from custom_components.magicmirror.models import Entity 21 | 22 | 23 | async def async_setup_entry( 24 | hass: HomeAssistant, 25 | entry: ConfigEntry, 26 | async_add_entities: AddEntitiesCallback, 27 | ) -> None: 28 | """Add MagicMirror entities from a config_entry.""" 29 | coordinator: MagicMirrorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] 30 | 31 | async_add_entities( 32 | [ 33 | MagicMirrorLight( 34 | coordinator, 35 | LightEntityDescription( 36 | key=Entity.MONITOR_STATUS.value, 37 | name="MagicMirror Monitor", 38 | ), 39 | ) 40 | ] 41 | ) 42 | 43 | 44 | class MagicMirrorLight(CoordinatorEntity, LightEntity): 45 | """Define a MagicMirror.""" 46 | 47 | monitor_state: bool 48 | brightness_state: int 49 | coordinator: MagicMirrorDataUpdateCoordinator 50 | 51 | def __init__( 52 | self, 53 | coordinator: MagicMirrorDataUpdateCoordinator, 54 | description: LightEntityDescription, 55 | ) -> None: 56 | """Initialize.""" 57 | super().__init__(coordinator) 58 | self.coordinator = coordinator 59 | self.entity_description = description 60 | self._attr_unique_id = f"{description.key}" 61 | self._attr_device_info = coordinator._attr_device_info 62 | 63 | self.color_mode = ColorMode.BRIGHTNESS 64 | self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} 65 | 66 | self.update_from_data() 67 | 68 | @property 69 | def is_on(self) -> bool: 70 | """Return true if the switch is on.""" 71 | return self.monitor_state 72 | 73 | @property 74 | def icon(self) -> str: 75 | """Return the icon to use in the frontend.""" 76 | return ( 77 | "mdi:toggle-switch-outline" 78 | if self.is_on 79 | else "mdi:toggle-switch-off-outline" 80 | ) 81 | 82 | def update_from_data(self) -> None: 83 | """Update sensor data.""" 84 | coordinator_data = self.coordinator.data 85 | self.monitor_state = ( 86 | coordinator_data.__getattribute__(Entity.MONITOR_STATUS.value) == STATE_ON 87 | ) 88 | self.brightness_state = int( 89 | coordinator_data.__getattribute__(Entity.BRIGHTNESS.value) 90 | ) 91 | 92 | @callback 93 | def _handle_coordinator_update(self) -> None: 94 | """Handle data update.""" 95 | self.update_from_data() 96 | super()._handle_coordinator_update() 97 | 98 | async def async_turn_on(self, **kwargs: Any) -> None: 99 | """Turn the entity on.""" 100 | if ATTR_BRIGHTNESS in kwargs: 101 | await self.coordinator.api.brightness( 102 | ceil(kwargs[ATTR_BRIGHTNESS] * 100 / 255.0) 103 | ) 104 | 105 | await self.coordinator.api.monitor_on() 106 | self.monitor_state = True 107 | await self.coordinator.async_request_refresh() 108 | 109 | async def async_turn_off(self, **kwargs: Any) -> None: 110 | """Turn the entity off.""" 111 | await self.coordinator.api.monitor_off() 112 | self.monitor_state = False 113 | await self.coordinator.async_request_refresh() 114 | 115 | @property 116 | def brightness(self) -> int | None: 117 | """Return the brightness of the light.""" 118 | return ceil(self.brightness_state * 255 / 100) 119 | -------------------------------------------------------------------------------- /custom_components/magicmirror/switch.py: -------------------------------------------------------------------------------- 1 | """Switch entity for MagicMirror.""" 2 | 3 | from typing import Any 4 | 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.core import HomeAssistant, callback 7 | from homeassistant.helpers.device_registry import DeviceInfo 8 | from homeassistant.helpers.entity import ( 9 | ToggleEntity, 10 | ToggleEntityDescription, 11 | ) 12 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 13 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 14 | 15 | from custom_components.magicmirror.const import DOMAIN, LOGGER 16 | from custom_components.magicmirror.coordinator import MagicMirrorDataUpdateCoordinator 17 | from custom_components.magicmirror.models import ModuleDataResponse 18 | 19 | 20 | async def async_setup_entry( 21 | hass: HomeAssistant, 22 | entry: ConfigEntry, 23 | async_add_entities: AddEntitiesCallback, 24 | ) -> None: 25 | """Add MagicMirror entities from a config_entry.""" 26 | coordinator: MagicMirrorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] 27 | 28 | async_add_entities( 29 | MagicMirrorModuleSwitch(coordinator, module) 30 | for module in coordinator.data.modules 31 | ) 32 | 33 | 34 | class MagicMirrorSwitch(CoordinatorEntity, ToggleEntity): 35 | """Define a MagicMirror entity.""" 36 | 37 | sensor_data: bool 38 | coordinator: MagicMirrorDataUpdateCoordinator 39 | 40 | def __init__( 41 | self, 42 | coordinator: MagicMirrorDataUpdateCoordinator, 43 | description: ToggleEntityDescription, 44 | ) -> None: 45 | """Initialize.""" 46 | super().__init__(coordinator) 47 | self.coordinator = coordinator 48 | self.entity_description = description 49 | self._attr_unique_id = f"{description.key}" 50 | self._attr_device_info = coordinator._attr_device_info 51 | 52 | self.update_from_data() 53 | 54 | @property 55 | def is_on(self) -> bool: 56 | """Return true if the switch is on.""" 57 | return self.sensor_data 58 | 59 | @property 60 | def icon(self): 61 | """Return the icon to use in the frontend.""" 62 | return "mdi:toggle-switch" if self.is_on else "mdi:toggle-switch-off-outline" 63 | 64 | def update_from_data(self) -> None: 65 | """Update sensor data.""" 66 | self.sensor_data = self.coordinator.data.__getattribute__( 67 | self.entity_description.key 68 | ) 69 | 70 | @callback 71 | def _handle_coordinator_update(self) -> None: 72 | """Handle data update.""" 73 | self.update_from_data() 74 | super()._handle_coordinator_update() 75 | 76 | async def async_turn_on(self, **kwargs: Any) -> None: 77 | """Turn the entity on.""" 78 | LOGGER.error("Switch not implemented") 79 | self.sensor_data = True 80 | await self.coordinator.async_request_refresh() 81 | 82 | async def async_turn_off(self, **kwargs: Any) -> None: 83 | """Turn the entity off.""" 84 | LOGGER.error("Switch not implemented") 85 | self.sensor_data = False 86 | await self.coordinator.async_request_refresh() 87 | 88 | 89 | class MagicMirrorModuleSwitch(MagicMirrorSwitch): 90 | """Define a MagicMirrorModule entity.""" 91 | 92 | def __init__( 93 | self, 94 | coordinator: MagicMirrorDataUpdateCoordinator, 95 | module: ModuleDataResponse, 96 | ) -> None: 97 | """Initialize.""" 98 | super().__init__( 99 | coordinator, 100 | ToggleEntityDescription(key=module.name), 101 | ) 102 | self.module = module 103 | 104 | self.entity_id = f"switch.{module.identifier}" 105 | self._attr_name = ( 106 | "Module " + module.header if module.header is not None else module.name 107 | ) 108 | self._attr_unique_id = module.identifier 109 | self.update_from_data() 110 | 111 | @property 112 | def device_info(self) -> DeviceInfo | None: 113 | return DeviceInfo( 114 | name=self.entity_description.key, 115 | model=self.entity_description.key, 116 | manufacturer="MagicMirror", 117 | identifiers={(DOMAIN, self.entity_description.key)}, 118 | configuration_url=f"{self.coordinator.api.base_url}/remote.html", 119 | ) 120 | 121 | def update_from_data(self) -> None: 122 | for module in self.coordinator.data.modules: 123 | if module.name == self.entity_description.key: 124 | self.sensor_data = False if module.hidden else True 125 | return 126 | self.sensor_data = "unknown" 127 | 128 | async def async_turn_on(self, **kwargs: Any) -> None: 129 | """Turn the entity on.""" 130 | await self.coordinator.api.show_module(self.module.identifier) 131 | self.sensor_data = True 132 | await self.coordinator.async_request_refresh() 133 | 134 | async def async_turn_off(self, **kwargs: Any) -> None: 135 | """Turn the entity off.""" 136 | await self.coordinator.api.hide_module(self.module.identifier) 137 | self.sensor_data = False 138 | await self.coordinator.async_request_refresh() 139 | -------------------------------------------------------------------------------- /custom_components/magicmirror/models.py: -------------------------------------------------------------------------------- 1 | """Models for MagicMirror.""" 2 | 3 | from enum import Enum 4 | from typing import Any 5 | 6 | import attr 7 | 8 | from custom_components.magicmirror.const import LOGGER 9 | 10 | 11 | class Entity(Enum): 12 | """Enum for storing Entity.""" 13 | 14 | MONITOR_STATUS = "monitor_status" 15 | UPDATE_AVAILABLE = "update_available" 16 | BRIGHTNESS = "brightness" 17 | MODULES = "modules" 18 | 19 | REBOOT = "reboot" 20 | RESTART = "restart" 21 | REFRESH = "refresh" 22 | SHUTDOWN = "shutdown" 23 | 24 | 25 | class Services(Enum): 26 | """Enum for storing services.""" 27 | 28 | MINIMIZE = "minimize" 29 | FULLSCREEN_TOGGLE = "toggle_fullscreen" 30 | DEVTOOLS = "devtools" 31 | MODULE = "module" 32 | MODULE_ACTION = "module_action" 33 | MODULE_INSTALLED = "module_installed" # Create entity based on installed? 34 | MODULE_AVAILABLE = "module_available" 35 | MODULE_UPDATE = "module_update" 36 | MODULE_INSTALL = "module_install" 37 | 38 | 39 | class ActionsDict: 40 | """Class representing Actions.""" 41 | 42 | notification: str 43 | guessed: bool 44 | 45 | 46 | @attr.s(auto_attribs=True) 47 | class ModuleDataResponse: 48 | """Class representing Module Data Response.""" 49 | 50 | index: int 51 | identifier: str 52 | name: str 53 | path: str 54 | file: str 55 | configDeepMerge: bool 56 | header: str # optional 57 | config: str # dict 58 | classes: str 59 | hidden: bool 60 | lockStrings: str # List 61 | actions: str # optional # Dict[str, ActionsDict] 62 | 63 | @staticmethod 64 | def from_dict(data: dict[str, Any]) -> "ModuleDataResponse": 65 | """Transform data to dict.""" 66 | LOGGER.debug("ModuleDataResponse=%s", data) 67 | 68 | return ModuleDataResponse( 69 | index=data.get("index"), 70 | identifier=data.get("identifier"), 71 | name=data.get("name"), 72 | path=data.get("path"), 73 | file=data.get("file"), 74 | configDeepMerge=bool(data.get("configDeepMerge")), 75 | classes=data.get("classes"), 76 | hidden=bool(data.get("hidden")), 77 | header=data.get("header"), 78 | config=data.get("config"), 79 | lockStrings=data.get("lockStrings"), 80 | actions=data.get("actions"), 81 | ) 82 | 83 | 84 | @attr.s(auto_attribs=True) 85 | class ModuleUpdateResponse: 86 | """Class representing ModuleUpdateResponse.""" 87 | 88 | module: str 89 | result: bool 90 | remote: str 91 | lsremote: str 92 | behind: int 93 | 94 | @staticmethod 95 | def from_dict(data: dict[str, Any]) -> "ModuleUpdateResponse": 96 | """Transform data to dict.""" 97 | LOGGER.debug("ModuleUpdateResponse=%s", data) 98 | 99 | return ModuleUpdateResponse( 100 | module=data.get("module"), 101 | result=bool(data.get("result", False)), 102 | remote=data.get("remote") or "", 103 | lsremote=data.get("lsremote") or "", 104 | behind=int(data.get("behind", 0)), 105 | ) 106 | 107 | 108 | @attr.s(auto_attribs=True) 109 | class MagicMirrorData: 110 | """Class representing MagicMirrorData.""" 111 | 112 | monitor_status: str 113 | update_available: bool 114 | module_updates: list[ModuleUpdateResponse] 115 | brightness: int 116 | modules: list[ModuleDataResponse] 117 | 118 | 119 | @attr.s(auto_attribs=True) 120 | class ModuleResponse: 121 | """Class representing Module Response.""" 122 | 123 | success: bool 124 | data: list[ModuleDataResponse] 125 | 126 | @staticmethod 127 | def from_dict(data: dict[str, Any]) -> "ModuleResponse": 128 | """Transform data to dict.""" 129 | LOGGER.debug("ModuleResponse=%s", data) 130 | 131 | modules: list[ModuleDataResponse] = [] 132 | for module in data.get("data"): 133 | modules.append(ModuleDataResponse.from_dict(module)) 134 | 135 | return ModuleResponse( 136 | success=bool(data.get("success")), 137 | data=modules, 138 | ) 139 | 140 | 141 | @attr.s(auto_attribs=True) 142 | class MonitorResponse: 143 | """Class representing MagicMirror.""" 144 | 145 | success: bool 146 | monitor: str 147 | 148 | @staticmethod 149 | def from_dict(data: dict[str, Any]) -> "MonitorResponse": 150 | """Transform data to dict.""" 151 | LOGGER.debug("MonitorResponse=%s", data) 152 | 153 | return MonitorResponse( 154 | success=bool(data.get("success")), 155 | monitor=data.get("monitor"), 156 | ) 157 | 158 | 159 | @attr.s(auto_attribs=True) 160 | class Query: 161 | """Class representing Query.""" 162 | 163 | data: str 164 | 165 | @staticmethod 166 | def from_dict(query: dict[str, Any]) -> "Query": 167 | """Transform data to dict.""" 168 | LOGGER.debug("Query=%s", query) 169 | return Query(data=query.get("data")) 170 | 171 | 172 | @attr.s(auto_attribs=True) 173 | class ModuleUpdateResponses: 174 | """Class representing Module Response.""" 175 | 176 | success: bool 177 | result: list[ModuleUpdateResponse] 178 | 179 | @staticmethod 180 | def from_dict(data: dict[str, Any]) -> "ModuleUpdateResponses": 181 | """Transform data to dict.""" 182 | LOGGER.debug("ModuleUpdateResponses=%s", data) 183 | 184 | module_update: list[ModuleUpdateResponse] = [] 185 | for module in data.get("result"): 186 | module_update.append(ModuleUpdateResponse.from_dict(module)) 187 | 188 | return ModuleUpdateResponses( 189 | success=bool(data.get("success")), 190 | result=module_update, 191 | ) 192 | 193 | 194 | @attr.s(auto_attribs=True) 195 | class QueryResponse: 196 | """Class representing MagicMirror.""" 197 | 198 | success: bool 199 | result: Any 200 | query: Query 201 | 202 | @staticmethod 203 | def from_dict(data: dict[str, Any]) -> "QueryResponse": 204 | """Transform data to dict.""" 205 | LOGGER.debug("QueryResponse=%s", data) 206 | 207 | return QueryResponse( 208 | success=bool(data.get("success")), 209 | result=data.get("result"), 210 | query=Query.from_dict(data.get("query")), 211 | ) 212 | 213 | 214 | @attr.s(auto_attribs=True) 215 | class GenericResponse: 216 | """Class representing MagicMirror.""" 217 | 218 | success: bool 219 | 220 | @staticmethod 221 | def from_dict(data: dict[str, Any]) -> "GenericResponse": 222 | """Transform data to dict.""" 223 | LOGGER.debug("GenericResponse=%s", data) 224 | 225 | return GenericResponse( 226 | success=bool(data.get("success")), 227 | ) 228 | -------------------------------------------------------------------------------- /custom_components/magicmirror/update.py: -------------------------------------------------------------------------------- 1 | """Update for MagicMirror.""" 2 | 3 | from typing import Any 4 | 5 | from homeassistant.components.update import UpdateEntity, UpdateEntityFeature 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.const import STATE_ON, EntityCategory 8 | from homeassistant.core import HomeAssistant, callback 9 | from homeassistant.helpers.device_registry import DeviceInfo 10 | from homeassistant.helpers.entity import EntityDescription 11 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 12 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 13 | 14 | from custom_components.magicmirror.const import DOMAIN 15 | from custom_components.magicmirror.coordinator import MagicMirrorDataUpdateCoordinator 16 | from custom_components.magicmirror.models import ( 17 | Entity, 18 | ModuleDataResponse, 19 | ModuleUpdateResponse, 20 | ) 21 | 22 | OLD_VERSION = "outdated" 23 | LATEST_VERSION = "latest" 24 | 25 | 26 | async def async_setup_entry( 27 | hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback 28 | ) -> None: 29 | """Set up the MagicMirror update entities.""" 30 | coordinator: MagicMirrorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] 31 | 32 | async_add_entities( 33 | [ 34 | MagicMirrorUpdate( 35 | coordinator, 36 | EntityDescription( 37 | key=Entity.UPDATE_AVAILABLE.value, 38 | name="MagicMirror update", 39 | ), 40 | ) 41 | ] 42 | ) 43 | 44 | coordinator: MagicMirrorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] 45 | modules = list(coordinator.data.modules) 46 | updates = list(coordinator.data.module_updates) 47 | 48 | update_entities: list[MagicMirrorModuleUpdate] = [] 49 | for module in modules: 50 | for update in updates: 51 | if module.name == update.module: 52 | update_entities.append( 53 | MagicMirrorModuleUpdate(coordinator, module, update) 54 | ) 55 | 56 | async_add_entities(update_entities) 57 | 58 | 59 | class MagicMirrorUpdate(CoordinatorEntity, UpdateEntity): 60 | """MagicMirror Update class.""" 61 | 62 | coordinator: MagicMirrorDataUpdateCoordinator 63 | 64 | def __init__( 65 | self, 66 | coordinator: MagicMirrorDataUpdateCoordinator, 67 | description: EntityDescription, 68 | ) -> None: 69 | """Initialize update entity.""" 70 | super().__init__(coordinator) 71 | 72 | self.coordinator = coordinator 73 | self.entity_description = description 74 | 75 | self.sensor_data = self.get_sensor_data() 76 | 77 | self._attr_unique_id = f"{description.name}" 78 | self._attr_device_info = self.coordinator._attr_device_info 79 | self._attr_entity_category = EntityCategory.DIAGNOSTIC 80 | 81 | self._attr_title = description.name 82 | self._attr_release_url = ( 83 | "https://github.com/MichMich/MagicMirror/releases/latest" 84 | ) 85 | 86 | self._attr_latest_version = LATEST_VERSION 87 | self._attr_display_precision = 0 88 | 89 | def get_sensor_data(self) -> bool: 90 | """Get sensor data.""" 91 | state = self.coordinator.data.__getattribute__(self.entity_description.key) 92 | return state == STATE_ON 93 | 94 | @callback 95 | def _handle_coordinator_update(self) -> None: 96 | """Handle data update.""" 97 | self.sensor_data = self.get_sensor_data() 98 | super()._handle_coordinator_update() 99 | 100 | @property 101 | def installed_version(self) -> str: 102 | """Version installed and in use.""" 103 | return OLD_VERSION if self.sensor_data else LATEST_VERSION 104 | 105 | 106 | class MagicMirrorModuleUpdate(CoordinatorEntity, UpdateEntity): 107 | """MagicMirror Module Update class.""" 108 | 109 | module: ModuleDataResponse 110 | coordinator: MagicMirrorDataUpdateCoordinator 111 | 112 | def __init__( 113 | self, 114 | coordinator: MagicMirrorDataUpdateCoordinator, 115 | module: ModuleDataResponse, 116 | update: ModuleUpdateResponse, 117 | ) -> None: 118 | """Initialize update entity.""" 119 | super().__init__(coordinator) 120 | self.coordinator = coordinator 121 | self.entity_description = EntityDescription(key=module.name) 122 | 123 | self.module = module 124 | self._attr_name = f"{module.name} update" 125 | self._attr_unique_id = module.identifier 126 | self._attr_title = module.name 127 | self._attr_supported_features = ( 128 | UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS 129 | ) 130 | 131 | self.sensor_data = update 132 | self.entity_id = f"update.{module.name}" 133 | 134 | def get_sensor_data(self) -> ModuleUpdateResponse | None: 135 | """Get sensor data.""" 136 | for module in self.coordinator.data.module_updates: 137 | if self.module.name == module.module: 138 | return module 139 | return None 140 | 141 | @callback 142 | def _handle_coordinator_update(self) -> None: 143 | """Handle data update.""" 144 | self.sensor_data = self.get_sensor_data() 145 | super()._handle_coordinator_update() 146 | 147 | async def async_install( 148 | self, version: str | None, backup: bool, **kwargs: Any 149 | ) -> None: 150 | """Install update.""" 151 | self._attr_in_progress = True 152 | await self.coordinator.api.module_update(self.module.name) 153 | self._attr_in_progress = False 154 | 155 | @property 156 | def installed_version(self) -> str: 157 | """Version installed and in use.""" 158 | return ( 159 | OLD_VERSION 160 | if self.sensor_data is None or self.sensor_data.result 161 | else LATEST_VERSION 162 | ) 163 | 164 | @property 165 | def latest_version(self) -> str | None: 166 | """Latest version available for install.""" 167 | return LATEST_VERSION 168 | 169 | @property 170 | def release_url(self) -> str | None: 171 | """URL to the full release notes of the latest version available.""" 172 | return self.sensor_data.remote if self.sensor_data is not None else None 173 | 174 | @property 175 | def device_info(self) -> DeviceInfo | None: 176 | return DeviceInfo( 177 | name=self.entity_description.key, 178 | model=self.entity_description.key, 179 | manufacturer="MagicMirror", 180 | identifiers={(DOMAIN, self.entity_description.key)}, 181 | configuration_url=f"{self.coordinator.api.base_url}/remote.html", 182 | ) 183 | -------------------------------------------------------------------------------- /custom_components/magicmirror/api.py: -------------------------------------------------------------------------------- 1 | """MagicMirror API.""" 2 | 3 | from http import HTTPStatus 4 | from typing import Any 5 | 6 | import aiohttp 7 | 8 | from custom_components.magicmirror.const import LOGGER 9 | from custom_components.magicmirror.models import ( 10 | GenericResponse, 11 | ModuleResponse, 12 | ModuleUpdateResponses, 13 | MonitorResponse, 14 | QueryResponse, 15 | ) 16 | 17 | # Mirror control 18 | API_TEST = "api/test" 19 | API_MONITOR = "api/monitor" 20 | API_MONITOR_ON = f"{API_MONITOR}/on" 21 | API_MONITOR_OFF = f"{API_MONITOR}/off" 22 | API_MONITOR_STATUS = f"{API_MONITOR}/status" 23 | API_MONITOR_TOGGLE = f"{API_MONITOR}/toggle" 24 | 25 | API_SHUTDOWN = "api/shutdown" 26 | API_REBOOT = "api/reboot" 27 | API_RESTART = "api/restart" 28 | API_MINIMIZE = "api/minimize" 29 | API_TOGGLEFULLSCREEN = "api/togglefullscreen" 30 | API_DEVTOOLS = "api/devtools" 31 | API_REFRESH = "api/refresh" 32 | API_BRIGHTNESS = "api/brightness" 33 | 34 | # Module control 35 | API_MODULE = "api/module" 36 | API_MODULES = "api/modules" 37 | API_MODULE_INSTALLED = f"{API_MODULE}/installed" 38 | API_MODULE_AVAILABLE = f"{API_MODULE}/available" 39 | API_UPDATE_MODULE = "api/update" 40 | API_INSTALL_MODULE = "api/install" 41 | API_MM_UPDATE_AVAILABLE = "api/mmUpdateAvailable" 42 | API_UPDATE_AVAILABLE = "api/updateAvailable" 43 | 44 | # API 45 | API_CONFIG = "api/config" 46 | 47 | SWAGGER = "/api/docs/#/" 48 | 49 | 50 | class MagicMirrorApiClient: 51 | """Main class for handling connection with.""" 52 | 53 | def __init__( 54 | self, 55 | host: str, 56 | port: str, 57 | api_key: str, 58 | session: aiohttp.client.ClientSession | None = None, 59 | ) -> None: 60 | """Initialize connection with MagicMirror.""" 61 | self.host = host 62 | self.port = port 63 | self.api_key = api_key 64 | self._session = session 65 | 66 | self.base_url = f"http://{self.host}:{self.port}" 67 | self.headers = { 68 | "accept": "application/json", 69 | "Authorization": f"Bearer {self.api_key}", 70 | } 71 | 72 | async def handle_request(self, response) -> Any: 73 | """Handle request.""" 74 | LOGGER.debug("pre handle_request=%s", response) 75 | 76 | async with response as resp: 77 | if resp.status == HTTPStatus.FORBIDDEN: 78 | exception = f"Forbidden {resp}. Check for missing API-key." 79 | raise Exception(exception) 80 | 81 | if resp.status != HTTPStatus.OK: 82 | LOGGER.warning("Response not 200 OK %s", resp) 83 | data = None 84 | else: 85 | data = await resp.json() 86 | LOGGER.debug("post handle_request=%s", data) 87 | 88 | return data 89 | 90 | async def get(self, path: str) -> Any: 91 | """Get request.""" 92 | get_url = f"{self.base_url}/{path}" 93 | LOGGER.debug("GET url=%s. headers=%s", get_url, self.headers) 94 | 95 | if self._session is None: 96 | LOGGER.warning("There is no session") 97 | return None 98 | 99 | get = await self._session.get( 100 | url=get_url, 101 | headers=self.headers, 102 | ) 103 | 104 | LOGGER.debug("Response=%s", get) 105 | 106 | return await self.handle_request(get) 107 | 108 | async def system_call(self, path: str) -> None: 109 | """Get request.""" 110 | get_url = f"{self.base_url}/{path}" 111 | LOGGER.debug("GET url=%s. headers=%s", get_url, self.headers) 112 | 113 | if self._session is None: 114 | LOGGER.warning("There is no session") 115 | return 116 | 117 | try: 118 | await self._session.get( 119 | url=get_url, 120 | headers=self.headers, 121 | ) 122 | except (aiohttp.ClientConnectionError, aiohttp.ServerDisconnectedError) as e: 123 | LOGGER.error( 124 | "Connection error: %s. Check if the MagicMirror service is running.", e 125 | ) 126 | 127 | async def post(self, path: str, data: str | None = None) -> Any: 128 | """Post request.""" 129 | post_url = f"{self.base_url}/{path}" 130 | LOGGER.debug("POST url=%s. data=%s. headers=%s", post_url, data, self.headers) 131 | 132 | if self._session is None: 133 | LOGGER.warning("There is no session") 134 | return None 135 | 136 | post = ( 137 | await self._session.post( 138 | url=post_url, 139 | headers=self.headers, 140 | data=data, 141 | ), 142 | ) 143 | 144 | LOGGER.debug("Response=%s", post) 145 | 146 | return await self.handle_request(post) 147 | 148 | async def api_test(self) -> GenericResponse: 149 | """Test api.""" 150 | return GenericResponse.from_dict(await self.get(API_TEST)) 151 | 152 | async def mm_update_available(self) -> QueryResponse: 153 | """Get update available status.""" 154 | return QueryResponse.from_dict(await self.get(API_MM_UPDATE_AVAILABLE)) 155 | 156 | async def update_available(self) -> ModuleUpdateResponses: 157 | """Get update available status.""" 158 | response = await self.get(API_UPDATE_AVAILABLE) 159 | if response is None: 160 | return ModuleUpdateResponses(success=False, result=[]) 161 | return ModuleUpdateResponses.from_dict(response) 162 | 163 | async def monitor_status(self) -> MonitorResponse: 164 | """Get monitor status.""" 165 | return MonitorResponse.from_dict(await self.get(API_MONITOR_STATUS)) 166 | 167 | async def get_modules(self) -> ModuleResponse: 168 | """Get module status.""" 169 | return ModuleResponse.from_dict(await self.get(API_MODULE)) 170 | 171 | async def monitor_on(self) -> Any: 172 | """Turn on monitor.""" 173 | return MonitorResponse.from_dict(await self.get(API_MONITOR_ON)) 174 | 175 | async def monitor_off(self) -> Any: 176 | """Turn off monitor.""" 177 | return MonitorResponse.from_dict(await self.get(API_MONITOR_OFF)) 178 | 179 | async def monitor_toggle(self) -> Any: 180 | """Toggle monitor.""" 181 | return MonitorResponse.from_dict(await self.get(API_MONITOR_TOGGLE)) 182 | 183 | async def shutdown(self) -> None: 184 | """Shutdown.""" 185 | await self.system_call(API_SHUTDOWN) 186 | 187 | async def reboot(self) -> None: 188 | """Reboot.""" 189 | await self.system_call(API_REBOOT) 190 | 191 | async def restart(self) -> None: 192 | """Restart.""" 193 | await self.system_call(API_RESTART) 194 | 195 | async def refresh(self) -> None: 196 | """Refresh.""" 197 | await self.system_call(API_REFRESH) 198 | 199 | async def minimize(self) -> Any: 200 | """Minimize.""" 201 | return await self.get(API_MINIMIZE) 202 | 203 | async def toggle_fullscreen(self) -> Any: 204 | """Toggle fullscreen.""" 205 | return await self.get(API_TOGGLEFULLSCREEN) 206 | 207 | async def devtools(self) -> Any: 208 | """Devtools.""" 209 | return await self.get(API_DEVTOOLS) 210 | 211 | async def brightness(self, brightness: str) -> Any: 212 | """Brightness.""" 213 | return await self.get(f"{API_BRIGHTNESS}/{brightness}") 214 | 215 | async def get_brightness(self) -> QueryResponse: 216 | """Brightness.""" 217 | return QueryResponse.from_dict(await self.get(API_BRIGHTNESS)) 218 | 219 | async def module(self, module_name: str) -> Any: 220 | """Endpoint for module.""" 221 | return await self.get(f"{API_MODULE}/{module_name}") 222 | 223 | async def module_action(self, module_name: str, action) -> Any: 224 | """Endpoint for module action.""" 225 | return await self.get(f"{API_MODULE}/{module_name}/{action}") 226 | 227 | async def module_update(self, module_name: str) -> Any: 228 | """Endpoint for module update.""" 229 | return await self.get(f"{API_UPDATE_MODULE}/{module_name}") 230 | 231 | async def modules(self) -> Any: 232 | """Endpoint for modules.""" 233 | return await self.get(API_MODULES) 234 | 235 | async def module_installed(self) -> Any: 236 | """Endpoint for module installed.""" 237 | return await self.get(API_MODULE_INSTALLED) 238 | 239 | async def module_available(self) -> Any: 240 | """Endpoint for module available.""" 241 | return await self.get(API_MODULE_AVAILABLE) 242 | 243 | async def module_install(self, data) -> Any: 244 | """Endpoint for module install.""" 245 | return await self.post(API_INSTALL_MODULE, data=data) 246 | 247 | async def config(self) -> Any: 248 | """Config.""" 249 | return await self.get(API_CONFIG) 250 | 251 | async def show_module(self, module) -> Any: 252 | """Show module.""" 253 | return await self.get(f"{API_MODULE}/{module}/show") 254 | 255 | async def hide_module(self, module) -> Any: 256 | """Hide module.""" 257 | return await self.get(f"{API_MODULE}/{module}/hide") 258 | 259 | async def alert( 260 | self, 261 | title: str, 262 | msg: str, 263 | timer: str, 264 | dropdown: bool = False, 265 | ) -> Any: 266 | """Notification screen.""" 267 | alert = "&type=notification" if dropdown else "" 268 | 269 | return await self.get( 270 | f"{API_MODULE}/alert/showalert?title={title}&message={msg}&timer={timer}{alert}" 271 | ) 272 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------