diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index ef8a9b4..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM python:3.7 - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - git \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -RUN python -m pip install --upgrade colorlog black pylint -RUN python -m pip install --upgrade git+https://github.com/home-assistant/home-assistant@dev -RUN cd && mkdir -p /config/custom_components - - -WORKDIR /workspace - -# Set the default shell to bash instead of sh -ENV SHELL /bin/bash \ No newline at end of file diff --git a/.devcontainer/README.md b/.devcontainer/README.md deleted file mode 100644 index f52282a..0000000 --- a/.devcontainer/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# Devcontainer - -_The easiest way to contribute to and/or test this repository._ - -## Requirements - -- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -- [docker](https://docs.docker.com/install/) -- [VS Code](https://code.visualstudio.com/) -- [Remote - Containers (VSC Extention)](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) - -[More info about requirements and devcontainer in general](https://code.visualstudio.com/docs/remote/containers#_getting-started) - -## How to use Devcontainer for development/test - -1. Make sure your computer meets the requirements. -1. Fork this repository. -1. Clone the repository to your computer. -1. Open the repository using VS Code. - -When you open this repository with VSCode and your computer meets the requirements you are asked to "Reopen in Container", do that. - -![reopen](images/reopen.png) - -If you don't see this notification, open the command pallet (ctrl+shift+p) and select `Remote-Containers: Reopen Folder in Container`. - -_It will now build the devcontainer._ - -The container have some "tasks" to help you testing your changes. - -## Custom Tasks in this repository - -_Start "tasks" by opening the the command pallet (ctrl+shift+p) and select `Tasks: Run Task`_ - -Running tasks like `Start Home Assistant on port 8124` can be restarted by opening the the command pallet (ctrl+shift+p) and select `Tasks: Restart Running Task`, then select the task you want to restart. - -### Start Home Assistant on port 8124 - -This will copy the configuration and the integration files to the expected location in the container. - -And start up Home Assistant on [port 8124.](http://localhost:8124) - -### Upgrade Home Assistant to latest dev - -This will upgrade Home Assistant to the latest dev version. - -### Set Home Assistant Version - -This allows you to specify a version of Home Assistant to install inside the devcontainer. - -### Home Assistant Config Check - -This runs a config check to make sure your config is valid. diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index adbd88c..e693ddc 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -2,19 +2,4 @@ default_config: logger: default: error logs: - custom_components.blueprint: debug - - - -blueprint: - username: my_username - password: my_password - binary_sensor: - - enabled: true - name: My custom name - sensor: - - enabled: true - name: My custom name - switch: - - enabled: true - name: My custom name \ No newline at end of file + custom_components.blueprint: debug \ No newline at end of file diff --git a/.devcontainer/custom_component_helper b/.devcontainer/custom_component_helper deleted file mode 100644 index 189d759..0000000 --- a/.devcontainer/custom_component_helper +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -function StartHomeAssistant { - echo "Copy configuration.yaml" - cp -f .devcontainer/configuration.yaml /config || echo ".devcontainer/configuration.yaml are missing!" exit 1 - - echo "Copy the custom component" - rm -R /config/custom_components/ || echo "" - cp -r custom_components /config/custom_components/ || echo "Could not copy the custom_component" exit 1 - - echo "Start Home Assistant" - hass -c /config -} - -function UpdgradeHomeAssistantDev { - python -m pip install --upgrade git+https://github.com/home-assistant/home-assistant@dev -} - -function SetHomeAssistantVersion { - read -p 'Version: ' version - python -m pip install --upgrade homeassistant==$version -} - -function HomeAssistantConfigCheck { - hass -c /config --script check_config -} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 906fb42..46a77f9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,18 +1,25 @@ // See https://aka.ms/vscode-remote/devcontainer.json for format details. { + "image": "ludeeus/container:integration", "context": "..", - "dockerFile": "Dockerfile", - "appPort": "8124:8123", + "appPort": [ + "9123:8123" + ], + "postCreateCommand": "dc install", "runArgs": [ - "-e", - "GIT_EDTIOR='code --wait'" + "-v", + "${env:HOME}${env:USERPROFILE}/.ssh:/tmp/.ssh" ], "extensions": [ "ms-python.python", + "github.vscode-pull-request-github", "tabnine.tabnine-vscode" ], "settings": { - "python.pythonPath": "/usr/local/bin/python", + "files.eol": "\n", + "editor.tabSize": 4, + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": "/usr/bin/python3", "python.linting.pylintEnabled": true, "python.linting.enabled": true, "python.formatting.provider": "black", diff --git a/.devcontainer/images/reopen.png b/.devcontainer/images/reopen.png deleted file mode 100644 index cbcec3c..0000000 Binary files a/.devcontainer/images/reopen.png and /dev/null differ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..94f480d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file diff --git a/.github/settings.yml b/.github/settings.yml deleted file mode 100644 index 717c121..0000000 --- a/.github/settings.yml +++ /dev/null @@ -1,23 +0,0 @@ -repository: - private: false - has_issues: true - has_projects: false - has_wiki: false - has_downloads: false - default_branch: master - allow_squash_merge: true - allow_merge_commit: false - allow_rebase_merge: false -labels: - - name: "Feature Request" - color: "fbca04" - - name: "Bug" - color: "b60205" - - name: "Wont Fix" - color: "ffffff" - - name: "Enhancement" - color: a2eeef - - name: "Documentation" - color: "008672" - - name: "Stale" - color: "930191" \ No newline at end of file diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/cron.yaml similarity index 51% rename from .github/workflows/hassfest.yaml rename to .github/workflows/cron.yaml index cf71cc2..ea26a76 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/cron.yaml @@ -1,14 +1,13 @@ -name: Validate with hassfest +name: Cron actions on: - push: - pull_request: schedule: - cron: '0 0 * * *' jobs: - validate: + hassfest: runs-on: "ubuntu-latest" + name: Validate with hassfest steps: - uses: "actions/checkout@v2" - - uses: home-assistant/actions/hassfest@master \ No newline at end of file + - uses: "home-assistant/actions/hassfest@master" \ No newline at end of file diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml new file mode 100644 index 0000000..437049d --- /dev/null +++ b/.github/workflows/pull.yml @@ -0,0 +1,23 @@ +name: Pull actions + +on: + pull_request: + +jobs: + hassfest: + runs-on: "ubuntu-latest" + name: Validate with hassfest + steps: + - uses: "actions/checkout@v2" + - uses: "home-assistant/actions/hassfest@master" + + style: + runs-on: "ubuntu-latest" + name: Check style formatting + steps: + - uses: "actions/checkout@v2" + - uses: "actions/setup-python@v1" + with: + python-version: "3.x" + - run: python3 -m pip install black + - run: black . \ No newline at end of file diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..aacb233 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,26 @@ +name: Push actions + +on: + push: + branches: + - master + - dev + +jobs: + hassfest: + runs-on: "ubuntu-latest" + name: Validate with hassfest + steps: + - uses: "actions/checkout@v2" + - uses: "home-assistant/actions/hassfest@master" + + style: + runs-on: "ubuntu-latest" + name: Check style formatting + steps: + - uses: "actions/checkout@v2" + - uses: "actions/setup-python@v1" + with: + python-version: "3.x" + - run: python3 -m pip install black + - run: black . \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 31504d9..ad1c9bf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,59 +2,27 @@ "version": "2.0.0", "tasks": [ { - "label": "Start Home Assistant on port 8124", + "label": "Run Home Assistant on port 9123", "type": "shell", - "command": "source .devcontainer/custom_component_helper && StartHomeAssistant", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, + "command": "dc start", + "problemMatcher": [] + }, + { + "label": "Run Home Assistant configuration against /config", + "type": "shell", + "command": "dc check", "problemMatcher": [] }, { "label": "Upgrade Home Assistant to latest dev", "type": "shell", - "command": "source .devcontainer/custom_component_helper && UpdgradeHomeAssistantDev", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, + "command": "dc install", "problemMatcher": [] }, { - "label": "Set Home Assistant Version", + "label": "Install a spesific version of Home Assistant", "type": "shell", - "command": "source .devcontainer/custom_component_helper && SetHomeAssistantVersion", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Home Assistant Config Check", - "type": "shell", - "command": "source .devcontainer/custom_component_helper && HomeAssistantConfigCheck", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, + "command": "dc set-version", "problemMatcher": [] } ] diff --git a/LICENSE b/LICENSE index 8d266b9..8a418c6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Joakim Sørensen @ludeeus +Copyright (c) 2020 Joakim Sørensen @ludeeus Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b552046..5f9716a 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,7 @@ Platform | Description 4. Download _all_ the files from the `custom_components/blueprint/` directory (folder) in this repository. 5. Place the files you downloaded in the new directory (folder) you created. 6. Restart Home Assistant -7. Choose: - - Add `blueprint:` to your HA configuration. - - In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Blueprint" +7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Blueprint" Using your HA configuration directory (folder) as a starting point you should now also have this: @@ -98,54 +96,9 @@ custom_components/blueprint/sensor.py custom_components/blueprint/switch.py ``` -## Example configuration.yaml +## Configuration is done in the UI -```yaml -blueprint: - username: my_username - password: my_password - binary_sensor: - - enabled: true - name: My custom name - sensor: - - enabled: true - name: My custom name - switch: - - enabled: true - name: My custom name -``` - -## Configuration options - -Key | Type | Required | Description --- | -- | -- | -- -`username` | `string` | `False` | Username for the client. -`password` | `string` | `False` | Password for the client. -`binary_sensor` | `list` | `False` | Configuration for the `binary_sensor` platform. -`sensor` | `list` | `False` | Configuration for the `sensor` platform. -`switch` | `list` | `False` | Configuration for the `switch` platform. - -### Configuration options for `binary_sensor` list - -Key | Type | Required | Default | Description --- | -- | -- | -- | -- -`enabled` | `boolean` | `False` | `True` | Boolean to enable/disable the platform. -`name` | `string` | `False` | `blueprint` | Custom name for the entity. - -### Configuration options for `sensor` list - -Key | Type | Required | Default | Description --- | -- | -- | -- | -- -`enabled` | `boolean` | `False` | `True` | Boolean to enable/disable the platform. -`name` | `string` | `False` | `blueprint` | Custom name for the entity. - - -### Configuration options for `switch` list - -Key | Type | Required | Default | Description --- | -- | -- | -- | -- -`enabled` | `boolean` | `False` | `True` | Boolean to enable/disable the platform. -`name` | `string` | `False` | `blueprint` | Custom name for the entity. + ## Contributions are welcome! diff --git a/custom_components/blueprint/.translations/en.json b/custom_components/blueprint/.translations/en.json index c9aeae6..a266873 100644 --- a/custom_components/blueprint/.translations/en.json +++ b/custom_components/blueprint/.translations/en.json @@ -13,9 +13,17 @@ }, "error": { "auth": "Username/Password is wrong." - }, - "abort": { - "single_instance_allowed": "Only a single configuration of Blueprint is allowed." + } + }, + "options": { + "step": { + "user": { + "data": { + "binary_sensor": "Binary sensor enabled", + "sensor": "Sensor enabled", + "switch": "Switch enabled" + } + } } } } \ No newline at end of file diff --git a/custom_components/blueprint/.translations/nb.json b/custom_components/blueprint/.translations/nb.json index 3b47f66..6151e02 100644 --- a/custom_components/blueprint/.translations/nb.json +++ b/custom_components/blueprint/.translations/nb.json @@ -13,9 +13,17 @@ }, "error": { "auth": "Brukernavn/Passord er feil." - }, - "abort": { - "single_instance_allowed": "Du kan konfigurere Blueprint kun en gang." + } + }, + "options": { + "step": { + "user": { + "data": { + "binary_sensor": "Binær sensor aktivert", + "sensor": "Sensor aktivert", + "switch": "Bryter aktivert" + } + } } } } \ No newline at end of file diff --git a/custom_components/blueprint/__init__.py b/custom_components/blueprint/__init__.py index 4528d63..3c3b94e 100644 --- a/custom_components/blueprint/__init__.py +++ b/custom_components/blueprint/__init__.py @@ -1,244 +1,107 @@ """ -Component to integrate with blueprint. +Custom integration to integrate blueprint with Home Assistant. -For more details about this component, please refer to +For more details about this integration, please refer to https://github.com/custom-components/blueprint """ -import os -from datetime import timedelta +import asyncio import logging -import voluptuous as vol -from homeassistant import config_entries -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.util import Throttle +from datetime import timedelta +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from sampleclient.client import Client -from integrationhelper.const import CC_STARTUP_VERSION from .const import ( - CONF_BINARY_SENSOR, - CONF_ENABLED, - CONF_NAME, CONF_PASSWORD, - CONF_SENSOR, - CONF_SWITCH, CONF_USERNAME, - DEFAULT_NAME, - DOMAIN_DATA, DOMAIN, - ISSUE_URL, PLATFORMS, - REQUIRED_FILES, - VERSION, + STARTUP_MESSAGE, ) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) -BINARY_SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_ENABLED, default=True): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) -SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_ENABLED, default=True): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - -SWITCH_SCHEMA = vol.Schema( - { - vol.Optional(CONF_ENABLED, default=True): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_BINARY_SENSOR): vol.All( - cv.ensure_list, [BINARY_SENSOR_SCHEMA] - ), - vol.Optional(CONF_SENSOR): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), - vol.Optional(CONF_SWITCH): vol.All(cv.ensure_list, [SWITCH_SCHEMA]), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up this component using YAML.""" - if config.get(DOMAIN) is None: - # We get here if the integration is set up using config flow - return True - - # Print startup message - _LOGGER.info( - CC_STARTUP_VERSION.format(name=DOMAIN, version=VERSION, issue_link=ISSUE_URL) - ) - - # Check that all required files are present - file_check = await check_files(hass) - if not file_check: - return False - - # Create DATA dict - hass.data[DOMAIN_DATA] = {} - - # Get "global" configuration. - username = config[DOMAIN].get(CONF_USERNAME) - password = config[DOMAIN].get(CONF_PASSWORD) - - # Configure the client. - client = Client(username, password) - hass.data[DOMAIN_DATA]["client"] = BlueprintData(hass, client) - - # Load platforms - for platform in PLATFORMS: - # Get platform specific configuration - platform_config = config[DOMAIN].get(platform, {}) - - # If platform is not enabled, skip. - if not platform_config: - continue - - for entry in platform_config: - entry_config = entry - - # If entry is not enabled, skip. - if not entry_config[CONF_ENABLED]: - continue - - hass.async_create_task( - discovery.async_load_platform( - hass, platform, DOMAIN, entry_config, config - ) - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} - ) - ) +async def async_setup(hass: HomeAssistant, config: Config): + """Set up this integration using YAML is not supported.""" return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up this integration using UI.""" - conf = hass.data.get(DOMAIN_DATA) - if config_entry.source == config_entries.SOURCE_IMPORT: - if conf is None: - hass.async_create_task( - hass.config_entries.async_remove(config_entry.entry_id) + if hass.data.get(DOMAIN) is None: + hass.data.setdefault(DOMAIN, {}) + _LOGGER.info(STARTUP_MESSAGE) + + username = entry.data.get(CONF_USERNAME) + password = entry.data.get(CONF_PASSWORD) + + coordinator = BlueprintDataUpdateCoordinator( + hass, username=username, password=password + ) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = coordinator + + for platform in PLATFORMS: + if entry.options.get(platform, True): + coordinator.platforms.append(platform) + hass.async_add_job( + hass.config_entries.async_forward_entry_setup(entry, platform) ) - return False - - # Print startup message - _LOGGER.info( - CC_STARTUP_VERSION.format(name=DOMAIN, version=VERSION, issue_link=ISSUE_URL) - ) - - # Check that all required files are present - file_check = await check_files(hass) - if not file_check: - return False - - # Create DATA dict - hass.data[DOMAIN_DATA] = {} - - # Get "global" configuration. - username = config_entry.data.get(CONF_USERNAME) - password = config_entry.data.get(CONF_PASSWORD) - - # Configure the client. - client = Client(username, password) - hass.data[DOMAIN_DATA]["client"] = BlueprintData(hass, client) - - # Add binary_sensor - hass.async_add_job( - hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") - ) - - # Add sensor - hass.async_add_job( - hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - ) - - # Add switch - hass.async_add_job( - hass.config_entries.async_forward_entry_setup(config_entry, "switch") - ) + entry.add_update_listener(async_reload_entry) return True -class BlueprintData: - """This class handle communication and stores the data.""" +class BlueprintDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the API.""" - def __init__(self, hass, client): - """Initialize the class.""" - self.hass = hass - self.client = client + def __init__(self, hass, username, password): + """Initialize.""" + self.api = Client(username, password) + self.platforms = [] - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def update_data(self): - """Update data.""" - # This is where the main logic to update platform data goes. + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self): + """Update data via library.""" try: - data = self.client.get_data() - self.hass.data[DOMAIN_DATA]["data"] = data - except Exception as error: # pylint: disable=broad-except - _LOGGER.error("Could not update data - %s", error) + data = await self.api.async_get_data() + return data.get("data", {}) + except Exception as exception: + raise UpdateFailed(exception) -async def check_files(hass): - """Return bool that indicates if all files are present.""" - # Verify that the user downloaded all files. - base = f"{hass.config.path()}/custom_components/{DOMAIN}/" - missing = [] - for file in REQUIRED_FILES: - fullpath = "{}{}".format(base, file) - if not os.path.exists(fullpath): - missing.append(file) - - if missing: - _LOGGER.critical("The following files are missing: %s", str(missing)) - returnvalue = False - else: - returnvalue = True - - return returnvalue - - -async def async_remove_entry(hass, config_entry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Handle removal of an entry.""" - try: - await hass.config_entries.async_forward_entry_unload( - config_entry, "binary_sensor" + coordinator = hass.data[DOMAIN][entry.entry_id] + unloaded = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + if platform in coordinator.platforms + ] ) - _LOGGER.info( - "Successfully removed binary_sensor from the blueprint integration" - ) - except ValueError: - pass + ) + if unloaded: + hass.data[DOMAIN].pop(entry.entry_id) - try: - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - _LOGGER.info("Successfully removed sensor from the blueprint integration") - except ValueError: - pass + return unloaded - try: - await hass.config_entries.async_forward_entry_unload(config_entry, "switch") - _LOGGER.info("Successfully removed switch from the blueprint integration") - except ValueError: - pass + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Reload config entry.""" + await async_unload_entry(hass, entry) + await async_setup_entry(hass, entry) diff --git a/custom_components/blueprint/binary_sensor.py b/custom_components/blueprint/binary_sensor.py index b83ba77..ebb8411 100644 --- a/custom_components/blueprint/binary_sensor.py +++ b/custom_components/blueprint/binary_sensor.py @@ -1,73 +1,28 @@ """Binary sensor platform for blueprint.""" from homeassistant.components.binary_sensor import BinarySensorDevice -from .const import ( - ATTRIBUTION, + +from custom_components.blueprint.const import ( + BINARY_SENSOR, BINARY_SENSOR_DEVICE_CLASS, DEFAULT_NAME, - DOMAIN_DATA, DOMAIN, ) +from custom_components.blueprint.entity import BlueprintEntity -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -): # pylint: disable=unused-argument +async def async_setup_entry(hass, entry, async_add_devices): """Setup binary_sensor platform.""" - async_add_entities([BlueprintBinarySensor(hass, discovery_info)], True) + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_devices([BlueprintBinarySensor(coordinator, entry)]) -async def async_setup_entry(hass, config_entry, async_add_devices): - """Setup sensor platform.""" - async_add_devices([BlueprintBinarySensor(hass, {})], True) - - -class BlueprintBinarySensor(BinarySensorDevice): +class BlueprintBinarySensor(BlueprintEntity, BinarySensorDevice): """blueprint binary_sensor class.""" - def __init__(self, hass, config): - self.hass = hass - self.attr = {} - self._status = False - self._name = config.get("name", DEFAULT_NAME) - - async def async_update(self): - """Update the binary_sensor.""" - # Send update "signal" to the component - await self.hass.data[DOMAIN_DATA]["client"].update_data() - - # Get new data (if any) - updated = self.hass.data[DOMAIN_DATA]["data"].get("data", {}) - - # Check the data and update the value. - if updated.get("bool_on") is None: - self._status = self._status - else: - self._status = updated.get("bool_on") - - # Set/update attributes - self.attr["attribution"] = ATTRIBUTION - self.attr["time"] = str(updated.get("time")) - self.attr["static"] = updated.get("static") - - @property - def unique_id(self): - """Return a unique ID to use for this binary_sensor.""" - return ( - "0919a0cd-745c-48fd" - ) # Don't hard code this, use something from the device/service. - - @property - def device_info(self): - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Blueprint", - } - @property def name(self): """Return the name of the binary_sensor.""" - return self._name + return f"{DEFAULT_NAME}_{BINARY_SENSOR}" @property def device_class(self): @@ -77,9 +32,4 @@ class BlueprintBinarySensor(BinarySensorDevice): @property def is_on(self): """Return true if the binary_sensor is on.""" - return self._status - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self.attr + return self.coordinator.data.get("bool_on", False) diff --git a/custom_components/blueprint/config_flow.py b/custom_components/blueprint/config_flow.py index 92a0e0e..567e351 100644 --- a/custom_components/blueprint/config_flow.py +++ b/custom_components/blueprint/config_flow.py @@ -4,12 +4,17 @@ from collections import OrderedDict import voluptuous as vol from sampleclient.client import Client from homeassistant import config_entries +from homeassistant.core import callback -from .const import DOMAIN +from custom_components.blueprint.const import ( + DOMAIN, + CONF_PASSWORD, + CONF_USERNAME, + PLATFORMS, +) -@config_entries.HANDLERS.register(DOMAIN) -class BlueprintFlowHandler(config_entries.ConfigFlow): +class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Blueprint.""" VERSION = 1 @@ -24,17 +29,14 @@ class BlueprintFlowHandler(config_entries.ConfigFlow): ): # pylint: disable=dangerous-default-value """Handle a flow initialized by the user.""" self._errors = {} - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - if self.hass.data.get(DOMAIN): - return self.async_abort(reason="single_instance_allowed") - if user_input is not None: valid = await self._test_credentials( - user_input["username"], user_input["password"] + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] ) if valid: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) else: self._errors["base"] = "auth" @@ -42,42 +44,62 @@ class BlueprintFlowHandler(config_entries.ConfigFlow): return await self._show_config_form(user_input) + @staticmethod + @callback + def async_get_options_flow(config_entry): + return BlueprintOptionsFlowHandler(config_entry) + async def _show_config_form(self, user_input): """Show the configuration form to edit location data.""" - - # Defaults - username = "" - password = "" - - if user_input is not None: - if "username" in user_input: - username = user_input["username"] - if "password" in user_input: - password = user_input["password"] - - data_schema = OrderedDict() - data_schema[vol.Required("username", default=username)] = str - data_schema[vol.Required("password", default=password)] = str return self.async_show_form( - step_id="user", data_schema=vol.Schema(data_schema), errors=self._errors + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str,} + ), + errors=self._errors, ) - async def async_step_import(self, user_input): # pylint: disable=unused-argument - """Import a config entry. - Special type of import, we're not actually going to store any data. - Instead, we're going to rely on the values that are in config file. - """ - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - return self.async_create_entry(title="configuration.yaml", data={}) - async def _test_credentials(self, username, password): """Return true if credentials is valid.""" try: client = Client(username, password) - client.get_data() + await client.async_get_data() return True except Exception: # pylint: disable=broad-except pass return False + + +class BlueprintOptionsFlowHandler(config_entries.OptionsFlow): + """Blueprint config flow options handler.""" + + def __init__(self, config_entry): + """Initialize HACS options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the options.""" + return await self.async_step_user() + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + self.options.update(user_input) + return await self._update_options() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(x, default=self.options.get(x, True)): bool + for x in sorted(PLATFORMS) + } + ), + ) + + async def _update_options(self): + """Update config entry options.""" + return self.async_create_entry( + title=self.config_entry.data.get(CONF_USERNAME), data=self.options + ) diff --git a/custom_components/blueprint/const.py b/custom_components/blueprint/const.py index 45d6e9c..86530cb 100644 --- a/custom_components/blueprint/const.py +++ b/custom_components/blueprint/const.py @@ -1,20 +1,11 @@ """Constants for blueprint.""" # Base component constants +NAME = "Blueprint" DOMAIN = "blueprint" DOMAIN_DATA = f"{DOMAIN}_data" VERSION = "0.0.1" -PLATFORMS = ["binary_sensor", "sensor", "switch"] -REQUIRED_FILES = [ - ".translations/en.json", - "binary_sensor.py", - "const.py", - "config_flow.py", - "manifest.json", - "sensor.py", - "switch.py", -] + ISSUE_URL = "https://github.com/custom-components/blueprint/issues" -ATTRIBUTION = "Data from this is provided by blueprint." # Icons ICON = "mdi:format-quote-close" @@ -22,14 +13,28 @@ ICON = "mdi:format-quote-close" # Device classes BINARY_SENSOR_DEVICE_CLASS = "connectivity" -# Configuration -CONF_BINARY_SENSOR = "binary_sensor" -CONF_SENSOR = "sensor" -CONF_SWITCH = "switch" +# Platforms +BINARY_SENSOR = "binary_sensor" +SENSOR = "sensor" +SWITCH = "switch" +PLATFORMS = [BINARY_SENSOR, SENSOR, SWITCH] + + +# Configuration and options CONF_ENABLED = "enabled" -CONF_NAME = "name" CONF_USERNAME = "username" CONF_PASSWORD = "password" # Defaults DEFAULT_NAME = DOMAIN + + +STARTUP_MESSAGE = f""" +------------------------------------------------------------------- +{NAME} +Version: {VERSION} +This is a custom integration! +If you have any issues with this you need to open an issue here: +{ISSUE_URL} +------------------------------------------------------------------- +""" diff --git a/custom_components/blueprint/entity.py b/custom_components/blueprint/entity.py new file mode 100644 index 0000000..5c181d6 --- /dev/null +++ b/custom_components/blueprint/entity.py @@ -0,0 +1,54 @@ +"""BlueprintEntity class""" +from homeassistant.helpers import entity + +from custom_components.blueprint.const import DOMAIN, VERSION, NAME + + +class BlueprintEntity(entity.Entity): + def __init__(self, coordinator, config_entry): + self.coordinator = coordinator + self.config_entry = config_entry + + @property + def should_poll(self): + """No need to poll. Coordinator notifies entity of updates.""" + return False + + @property + def available(self): + """Return if entity is available.""" + return self.coordinator.last_update_success + + @property + def unique_id(self): + """Return a unique ID to use for this entity.""" + return self.config_entry.entry_id + + @property + def device_info(self): + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": NAME, + "model": VERSION, + "manufacturer": NAME, + } + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + "time": str(self.coordinator.data.get("time")), + "static": self.coordinator.data.get("static"), + } + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Disconnect from update signal.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) + + async def async_update(self): + """Update Brother entity.""" + await self.coordinator.async_request_refresh() diff --git a/custom_components/blueprint/manifest.json b/custom_components/blueprint/manifest.json index 11a22d0..5456514 100644 --- a/custom_components/blueprint/manifest.json +++ b/custom_components/blueprint/manifest.json @@ -8,7 +8,6 @@ "@ludeeus" ], "requirements": [ - "sampleclient", - "integrationhelper" + "sampleclient" ] } \ No newline at end of file diff --git a/custom_components/blueprint/sensor.py b/custom_components/blueprint/sensor.py index ee21f82..db1891c 100644 --- a/custom_components/blueprint/sensor.py +++ b/custom_components/blueprint/sensor.py @@ -1,79 +1,28 @@ """Sensor platform for blueprint.""" -from homeassistant.helpers.entity import Entity -from .const import ATTRIBUTION, DEFAULT_NAME, DOMAIN_DATA, ICON, DOMAIN +from custom_components.blueprint.const import DEFAULT_NAME, DOMAIN, ICON, SENSOR +from custom_components.blueprint.entity import BlueprintEntity -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -): # pylint: disable=unused-argument +async def async_setup_entry(hass, entry, async_add_devices): """Setup sensor platform.""" - async_add_entities([BlueprintSensor(hass, discovery_info)], True) + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_devices([BlueprintSensor(coordinator, entry)]) -async def async_setup_entry(hass, config_entry, async_add_devices): - """Setup sensor platform.""" - async_add_devices([BlueprintSensor(hass, {})], True) - - -class BlueprintSensor(Entity): +class BlueprintSensor(BlueprintEntity): """blueprint Sensor class.""" - def __init__(self, hass, config): - self.hass = hass - self.attr = {} - self._state = None - self._name = config.get("name", DEFAULT_NAME) - - async def async_update(self): - """Update the sensor.""" - # Send update "signal" to the component - await self.hass.data[DOMAIN_DATA]["client"].update_data() - - # Get new data (if any) - updated = self.hass.data[DOMAIN_DATA]["data"].get("data", {}) - - # Check the data and update the value. - if updated.get("static") is None: - self._state = self._state - else: - self._state = updated.get("static") - - # Set/update attributes - self.attr["attribution"] = ATTRIBUTION - self.attr["time"] = str(updated.get("time")) - self.attr["none"] = updated.get("none") - - @property - def unique_id(self): - """Return a unique ID to use for this sensor.""" - return ( - "0717a0cd-745c-48fd" - ) # Don't hard code this, use something from the device/service. - - @property - def device_info(self): - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Blueprint", - } - @property def name(self): """Return the name of the sensor.""" - return self._name + return f"{DEFAULT_NAME}_{SENSOR}" @property def state(self): """Return the state of the sensor.""" - return self._state + return self.coordinator.data.get("static") @property def icon(self): """Return the icon of the sensor.""" return ICON - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self.attr diff --git a/custom_components/blueprint/switch.py b/custom_components/blueprint/switch.py index c5a058c..9ff0543 100644 --- a/custom_components/blueprint/switch.py +++ b/custom_components/blueprint/switch.py @@ -1,72 +1,34 @@ """Switch platform for blueprint.""" from homeassistant.components.switch import SwitchDevice -from .const import ATTRIBUTION, DEFAULT_NAME, DOMAIN_DATA, ICON, DOMAIN + +from custom_components.blueprint.const import DEFAULT_NAME, DOMAIN, ICON, SWITCH + +from custom_components.blueprint.entity import BlueprintEntity -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -): # pylint: disable=unused-argument - """Setup switch platform.""" - async_add_entities([BlueprintBinarySwitch(hass, discovery_info)], True) - - -async def async_setup_entry(hass, config_entry, async_add_devices): +async def async_setup_entry(hass, entry, async_add_devices): """Setup sensor platform.""" - async_add_devices([BlueprintBinarySwitch(hass, {})], True) + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_devices([BlueprintBinarySwitch(coordinator, entry)]) -class BlueprintBinarySwitch(SwitchDevice): +class BlueprintBinarySwitch(BlueprintEntity, SwitchDevice): """blueprint switch class.""" - def __init__(self, hass, config): - self.hass = hass - self.attr = {} - self._status = False - self._name = config.get("name", DEFAULT_NAME) - - async def async_update(self): - """Update the switch.""" - # Send update "signal" to the component - await self.hass.data[DOMAIN_DATA]["client"].update_data() - - # Get new data (if any) - updated = self.hass.data[DOMAIN_DATA]["data"].get("data", {}) - - # Check the data and update the value. - self._status = self.hass.data[DOMAIN_DATA]["client"].client.something - - # Set/update attributes - self.attr["attribution"] = ATTRIBUTION - self.attr["time"] = str(updated.get("time")) - self.attr["static"] = updated.get("static") - async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument """Turn on the switch.""" - await self.hass.data[DOMAIN_DATA]["client"].client.change_something(True) + await self.coordinator.api.async_change_something(True) + await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument """Turn off the switch.""" - await self.hass.data[DOMAIN_DATA]["client"].client.change_something(False) - - @property - def unique_id(self): - """Return a unique ID to use for this switch.""" - return ( - "0818a0cd-745c-48fd" - ) # Don't hard code this, use something from the device/service. - - @property - def device_info(self): - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Blueprint", - } + await self.coordinator.api.async_change_something(False) + await self.coordinator.async_request_refresh() @property def name(self): """Return the name of the switch.""" - return self._name + return f"{DEFAULT_NAME}_{SWITCH}" @property def icon(self): @@ -76,9 +38,4 @@ class BlueprintBinarySwitch(SwitchDevice): @property def is_on(self): """Return true if the switch is on.""" - return self._status - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self.attr + return self.coordinator.api.something diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..e908271 --- /dev/null +++ b/hacs.json @@ -0,0 +1,5 @@ +{ + "name": "Blueprint", + "hacs": "0.19.0", + "homeassistant": "0.97.0" +} \ No newline at end of file diff --git a/info.md b/info.md index c893c7d..e4a2094 100644 --- a/info.md +++ b/info.md @@ -25,58 +25,14 @@ Platform | Description ## Installation 1. Click install. -1. Add `blueprint:` to your HA configuration. +1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Blueprint". {% endif %} -## Example configuration.yaml - -```yaml -blueprint: - username: my_username - password: my_password - binary_sensor: - - enabled: true - name: My custom name - sensor: - - enabled: true - name: My custom name - switch: - - enabled: true - name: My custom name -``` - -## Configuration options - -Key | Type | Required | Description --- | -- | -- | -- -`username` | `string` | `False` | Username for the client. -`password` | `string` | `False` | Password for the client. -`binary_sensor` | `list` | `False` | Configuration for the `binary_sensor` platform. -`sensor` | `list` | `False` | Configuration for the `sensor` platform. -`switch` | `list` | `False` | Configuration for the `switch` platform. - -### Configuration options for `binary_sensor` list - -Key | Type | Required | Default | Description --- | -- | -- | -- | -- -`enabled` | `boolean` | `False` | `True` | Boolean to enable/disable the platform. -`name` | `string` | `False` | `blueprint` | Custom name for the entity. - -### Configuration options for `sensor` list - -Key | Type | Required | Default | Description --- | -- | -- | -- | -- -`enabled` | `boolean` | `False` | `True` | Boolean to enable/disable the platform. -`name` | `string` | `False` | `blueprint` | Custom name for the entity. -### Configuration options for `switch` list - -Key | Type | Required | Default | Description --- | -- | -- | -- | -- -`enabled` | `boolean` | `False` | `True` | Boolean to enable/disable the platform. -`name` | `string` | `False` | `blueprint` | Custom name for the entity. +## Configuration is done in the UI + *** diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7019fc7..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -integrationhelper -sampleclient \ No newline at end of file