├── custom_components ├── example_light │ ├── __init__.py │ ├── manifest.json │ ├── README.md │ └── light.py ├── example_sensor │ ├── __init__.py │ ├── manifest.json │ ├── README.md │ └── sensor.py ├── expose_service_sync │ ├── services.yaml │ ├── manifest.json │ └── __init__.py ├── expose_service_async │ ├── services.yaml │ ├── manifest.json │ └── __init__.py ├── mqtt_basic_async │ ├── services.yaml │ ├── manifest.json │ └── __init__.py ├── mqtt_basic_sync │ ├── services.yaml │ ├── manifest.json │ └── __init__.py ├── detailed_hello_world_push │ ├── const.py │ ├── manifest.json │ ├── README.md │ ├── strings.json │ ├── translations │ │ └── en.json │ ├── __init__.py │ ├── hub.py │ ├── config_flow.py │ ├── sensor.py │ └── cover.py ├── hello_world │ ├── manifest.json │ └── __init__.py ├── hello_world_async │ ├── manifest.json │ └── __init__.py └── example_load_platform │ ├── manifest.json │ ├── README.md │ ├── __init__.py │ └── sensor.py ├── .github ├── dependabot.yml └── workflows │ └── hassfest.yaml ├── python_scripts ├── counter.py └── count_people_home.py ├── README.md ├── .gitignore └── LICENSE /custom_components/example_light/__init__.py: -------------------------------------------------------------------------------- 1 | """Example Lights integration.""" 2 | -------------------------------------------------------------------------------- /custom_components/example_sensor/__init__.py: -------------------------------------------------------------------------------- 1 | """The example sensor integration.""" 2 | -------------------------------------------------------------------------------- /custom_components/expose_service_sync/services.yaml: -------------------------------------------------------------------------------- 1 | demo: 2 | description: Demo service 3 | -------------------------------------------------------------------------------- /custom_components/expose_service_async/services.yaml: -------------------------------------------------------------------------------- 1 | demo: 2 | description: Demo service 3 | -------------------------------------------------------------------------------- /custom_components/mqtt_basic_async/services.yaml: -------------------------------------------------------------------------------- 1 | set_state: 2 | description: Set MQTT state 3 | fields: 4 | new_state: 5 | description: New state of the entity 6 | example: "on" 7 | -------------------------------------------------------------------------------- /custom_components/mqtt_basic_sync/services.yaml: -------------------------------------------------------------------------------- 1 | set_state: 2 | description: Set MQTT state 3 | fields: 4 | new_state: 5 | description: New state of the entity 6 | example: "on" 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "06:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /python_scripts/counter.py: -------------------------------------------------------------------------------- 1 | counter = hass.states.get('sensor.my_counter') 2 | 3 | if counter is None: 4 | value = 0 5 | else: 6 | value = int(counter.state) 7 | 8 | hass.states.set('sensor.my_counter', value + 1) 9 | -------------------------------------------------------------------------------- /custom_components/detailed_hello_world_push/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Detailed Hello World Push integration.""" 2 | 3 | # This is the internal name of the integration, it should also match the directory 4 | # name for the integration. 5 | DOMAIN = "detailed_hello_world_push" 6 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v6.0.1" 14 | - uses: home-assistant/actions/hassfest@master 15 | -------------------------------------------------------------------------------- /python_scripts/count_people_home.py: -------------------------------------------------------------------------------- 1 | home = 0 2 | for entity_id in hass.states.entity_ids('device_tracker'): 3 | state = hass.states.get(entity_id) 4 | if state.state == 'home': 5 | home = home + 1 6 | 7 | hass.states.set('sensor.people_home', home, { 8 | 'unit_of_measurement': 'people', 9 | 'friendly_name': 'People home' 10 | }) 11 | -------------------------------------------------------------------------------- /custom_components/hello_world/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "hello_world", 3 | "name": "Hello World", 4 | "codeowners": [], 5 | "dependencies": [], 6 | "documentation": "https://github.com/home-assistant/example-custom-config/tree/master/custom_components/hello_world/", 7 | "iot_class": "local_polling", 8 | "requirements": [], 9 | "version": "0.1.0" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/example_sensor/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "example_sensor", 3 | "name": "Example Sensor", 4 | "codeowners": [], 5 | "dependencies": [], 6 | "documentation": "https://github.com/home-assistant/example-custom-config/tree/master/custom_components/example_sensor/", 7 | "iot_class": "local_polling", 8 | "requirements": [], 9 | "version": "0.1.0" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/mqtt_basic_sync/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "mqtt_basic_sync", 3 | "name": "MQTT Basic Example", 4 | "codeowners": [], 5 | "dependencies": ["mqtt"], 6 | "documentation": "https://github.com/home-assistant/example-custom-config/tree/master/custom_components/mqtt_basic/", 7 | "iot_class": "local_polling", 8 | "requirements": [], 9 | "version": "0.1.0" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/example_light/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "example_light", 3 | "name": "Example Light", 4 | "codeowners": [], 5 | "dependencies": [], 6 | "documentation": "https://github.com/home-assistant/example-custom-config/tree/master/custom_components/example_light/", 7 | "iot_class": "local_polling", 8 | "requirements": ["awesomelights==1.2.3"], 9 | "version": "0.1.0" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/hello_world_async/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "hello_world_async", 3 | "name": "Hello World (async)", 4 | "codeowners": [], 5 | "dependencies": [], 6 | "documentation": "https://github.com/home-assistant/example-custom-config/tree/master/custom_components/hello_world_async/", 7 | "iot_class": "local_polling", 8 | "requirements": [], 9 | "version": "0.1.0" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/expose_service_sync/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "expose_service_sync", 3 | "name": "Expose Service (sync)", 4 | "codeowners": [], 5 | "dependencies": [], 6 | "documentation": "https://github.com/home-assistant/example-custom-config/tree/master/custom_components/expose_service_sync/", 7 | "iot_class": "local_polling", 8 | "requirements": [], 9 | "version": "0.1.0" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/example_load_platform/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "example_load_platform", 3 | "name": "Example Load Platform", 4 | "codeowners": [], 5 | "dependencies": [], 6 | "documentation": "https://github.com/home-assistant/example-custom-config/tree/master/custom_components/example_load_platform/", 7 | "iot_class": "local_polling", 8 | "requirements": [], 9 | "version": "0.1.0" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/expose_service_async/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "expose_service_async", 3 | "name": "Expose Service (async)", 4 | "codeowners": [], 5 | "dependencies": [], 6 | "documentation": "https://github.com/home-assistant/example-custom-config/tree/master/custom_components/expose_service_async/", 7 | "iot_class": "local_polling", 8 | "requirements": [], 9 | "version": "0.1.0" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/mqtt_basic_async/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "mqtt_basic_async", 3 | "name": "MQTT Basic Example (async)", 4 | "codeowners": [], 5 | "dependencies": ["mqtt"], 6 | "documentation": "https://github.com/home-assistant/example-custom-config/tree/master/custom_components/mqtt_basic_async/", 7 | "iot_class": "local_polling", 8 | "requirements": [], 9 | "version": "0.1.0" 10 | } 11 | -------------------------------------------------------------------------------- /custom_components/example_sensor/README.md: -------------------------------------------------------------------------------- 1 | # Example Sensor 2 | 3 | This is a minimum implementation of an integration providing a sensor measurement. 4 | 5 | ### Installation 6 | 7 | Copy this folder to `/custom_components/example_sensor/`. 8 | 9 | Add the following to your `configuration.yaml` file: 10 | 11 | ```yaml 12 | # Example configuration.yaml entry 13 | sensor: 14 | - platform: example_sensor 15 | ``` 16 | -------------------------------------------------------------------------------- /custom_components/detailed_hello_world_push/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "detailed_hello_world_push", 3 | "name": "Detailed Hello World Push", 4 | "codeowners": ["@sillyfrog"], 5 | "config_flow": true, 6 | "dependencies": [], 7 | "documentation": "https://www.home-assistant.io/integrations/detailed_hello_world_push", 8 | "homekit": {}, 9 | "iot_class": "local_push", 10 | "requirements": [], 11 | "ssdp": [], 12 | "version": "0.1.0", 13 | "zeroconf": [] 14 | } 15 | -------------------------------------------------------------------------------- /custom_components/example_load_platform/README.md: -------------------------------------------------------------------------------- 1 | # Load platform example 2 | 3 | This is an example of an integration loading its platforms from its own set up, and passing information on to the platforms. 4 | 5 | Use this approach only if your integration is configured solely via `configuration.yaml` and does not use config entries. 6 | 7 | ### Installation 8 | 9 | Copy this folder to `/custom_components/example_load_platform/`. 10 | 11 | Add the following entry in your `configuration.yaml`: 12 | 13 | ```yaml 14 | example_load_platform: 15 | ``` 16 | -------------------------------------------------------------------------------- /custom_components/detailed_hello_world_push/README.md: -------------------------------------------------------------------------------- 1 | # More "Level 2" Comprehensive async Push Integration Example 2 | 3 | This example aims to show the best practice for a more complete integration using **push** with async. 4 | 5 | It is based on a _cover_, emulating battery operated roller blinds. 6 | 7 | The example includes extensive comments (that should be removed if making a true integration), to guide you through what each field and property is for. It includes 2 sensors tied to the primary cover device. 8 | 9 | It's all implemented using a _push_ model in _async_. 10 | 11 | This example does not cover translations. 12 | -------------------------------------------------------------------------------- /custom_components/example_load_platform/__init__.py: -------------------------------------------------------------------------------- 1 | """Example Load Platform integration.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.core import HomeAssistant 5 | from homeassistant.helpers.discovery import load_platform 6 | from homeassistant.helpers.typing import ConfigType 7 | 8 | DOMAIN = 'example_load_platform' 9 | 10 | 11 | def setup(hass: HomeAssistant, config: ConfigType) -> bool: 12 | """Your controller/hub specific code.""" 13 | # Data that you want to share with your platforms 14 | hass.data[DOMAIN] = { 15 | 'temperature': 23 16 | } 17 | 18 | load_platform(hass, 'sensor', DOMAIN, {}, config) 19 | 20 | return True 21 | -------------------------------------------------------------------------------- /custom_components/example_light/README.md: -------------------------------------------------------------------------------- 1 | # Awesome Lights 2 | 3 | This integration shows how you would go ahead and integrate a physical light into Home Assistant. 4 | 5 | If you use this integration as a template, make sure you tweak the following places: 6 | 7 | - `manifest.json`: update the requirements to point at your Python library 8 | - `light.py`: update the code to interact with your library 9 | 10 | ### Installation 11 | 12 | Copy this folder to `/custom_components/example_light/`. 13 | 14 | Add the following entry in your `configuration.yaml`: 15 | 16 | ```yaml 17 | light: 18 | - platform: example_light 19 | host: HOST_HERE 20 | username: USERNAME_HERE 21 | password: PASSWORD_HERE_OR_secrets.yaml 22 | ``` 23 | -------------------------------------------------------------------------------- /custom_components/detailed_hello_world_push/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "host": "[%key:common::config_flow::data::host%]", 7 | "username": "[%key:common::config_flow::data::username%]", 8 | "password": "[%key:common::config_flow::data::password%]" 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 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Examples of Home Assistant custom config 2 | 3 | Home Assistant can be extended in many different ways. This repository contains a collection of examples how to customize Home Assistant. This repository is organized like your config directory, copying any file to your config directory in the same path as it is in this repo will allow you to use it. Refer to the header of each example for further instructions how to get started. 4 | 5 | - **Custom components:** these are components that Home Assistant can load by being referenced from `configuration.yaml` just like built-in components. 6 | - **Panels:** these are custom panels that can be included in the frontend using [the `panel_custom` component][panel-custom]. 7 | 8 | [panel-custom]: https://home-assistant.io/components/panel_custom/ 9 | -------------------------------------------------------------------------------- /custom_components/detailed_hello_world_push/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 5 | }, 6 | "error": { 7 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 8 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 9 | "unknown": "[%key:common::config_flow::error::unknown%]" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "host": "[%key:common::config_flow::data::host%]", 15 | "password": "[%key:common::config_flow::data::password%]", 16 | "username": "[%key:common::config_flow::data::username%]" 17 | } 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /custom_components/hello_world/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The "hello world" custom component. 3 | 4 | This component implements the bare minimum that a component should implement. 5 | 6 | Configuration: 7 | 8 | To use the hello_world component you will need to add the following to your 9 | configuration.yaml file. 10 | 11 | hello_world: 12 | """ 13 | from __future__ import annotations 14 | 15 | from homeassistant.core import HomeAssistant 16 | from homeassistant.helpers.typing import ConfigType 17 | 18 | # The domain of your component. Should be equal to the name of your component. 19 | DOMAIN = "hello_world" 20 | 21 | 22 | def setup(hass: HomeAssistant, config: ConfigType) -> bool: 23 | """Set up a skeleton component.""" 24 | # States are in the format DOMAIN.OBJECT_ID. 25 | hass.states.set('hello_world.Hello_World', 'Works!') 26 | 27 | # Return boolean to indicate that initialization was successfully. 28 | return True 29 | -------------------------------------------------------------------------------- /custom_components/expose_service_sync/__init__.py: -------------------------------------------------------------------------------- 1 | """Example of a custom component exposing a service.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | from homeassistant.core import HomeAssistant, ServiceCall 7 | from homeassistant.helpers.typing import ConfigType 8 | 9 | # The domain of your component. Should be equal to the name of your component. 10 | DOMAIN = "expose_service_sync" 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | def setup(hass: HomeAssistant, config: ConfigType) -> bool: 15 | """Set up the sync service example component.""" 16 | def my_service(call: ServiceCall) -> None: 17 | """My first service.""" 18 | _LOGGER.info('Received data', call.data) 19 | 20 | # Register our service with Home Assistant. 21 | hass.services.register(DOMAIN, 'demo', my_service) 22 | 23 | # Return boolean to indicate that initialization was successfully. 24 | return True 25 | -------------------------------------------------------------------------------- /custom_components/hello_world_async/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The "hello world" custom component. 3 | 4 | This component implements the bare minimum that a component should implement. 5 | 6 | Configuration: 7 | 8 | To use the hello_world component you will need to add the following to your 9 | configuration.yaml file. 10 | 11 | hello_world_async: 12 | """ 13 | from __future__ import annotations 14 | 15 | import asyncio 16 | 17 | from homeassistant.core import HomeAssistant 18 | from homeassistant.helpers.typing import ConfigType 19 | 20 | # The domain of your component. Should be equal to the name of your component. 21 | DOMAIN = "hello_world_async" 22 | 23 | 24 | @asyncio.coroutine 25 | def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 26 | """Setup our skeleton component.""" 27 | # States are in the format DOMAIN.OBJECT_ID. 28 | hass.states.async_set('hello_world_async.Hello_World', 'Works!') 29 | 30 | # Return boolean to indicate that initialization was successfully. 31 | return True 32 | -------------------------------------------------------------------------------- /custom_components/expose_service_async/__init__.py: -------------------------------------------------------------------------------- 1 | """Example of a custom component exposing a service.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | from homeassistant.core import HomeAssistant, ServiceCall, callback 7 | from homeassistant.helpers import config_validation as cv 8 | from homeassistant.helpers.typing import ConfigType 9 | 10 | # The domain of your component. Should be equal to the name of your component. 11 | DOMAIN = "expose_service_async" 12 | _LOGGER = logging.getLogger(__name__) 13 | 14 | # Use empty_config_schema because the component does not have any config options 15 | CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) 16 | 17 | 18 | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 19 | """Set up the an async service example component.""" 20 | @callback 21 | def my_service(call: ServiceCall) -> None: 22 | """My first service.""" 23 | _LOGGER.info('Received data', call.data) 24 | 25 | # Register our service with Home Assistant. 26 | hass.services.async_register(DOMAIN, 'demo', my_service) 27 | 28 | # Return boolean to indicate that initialization was successfully. 29 | return True 30 | -------------------------------------------------------------------------------- /custom_components/example_sensor/sensor.py: -------------------------------------------------------------------------------- 1 | """Platform for sensor integration.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.components.sensor import ( 5 | SensorDeviceClass, 6 | SensorEntity, 7 | SensorStateClass, 8 | ) 9 | from homeassistant.const import UnitOfTemperature 10 | from homeassistant.core import HomeAssistant 11 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 12 | from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 13 | 14 | 15 | def setup_platform( 16 | hass: HomeAssistant, 17 | config: ConfigType, 18 | add_entities: AddEntitiesCallback, 19 | discovery_info: DiscoveryInfoType | None = None 20 | ) -> None: 21 | """Set up the sensor platform.""" 22 | add_entities([ExampleSensor()]) 23 | 24 | 25 | class ExampleSensor(SensorEntity): 26 | """Representation of a Sensor.""" 27 | 28 | _attr_name = "Example Temperature" 29 | _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS 30 | _attr_device_class = SensorDeviceClass.TEMPERATURE 31 | _attr_state_class = SensorStateClass.MEASUREMENT 32 | 33 | def update(self) -> None: 34 | """Fetch new state data for the sensor. 35 | 36 | This is the only method that should fetch new data for Home Assistant. 37 | """ 38 | self._attr_native_value = 23 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /custom_components/detailed_hello_world_push/__init__.py: -------------------------------------------------------------------------------- 1 | """The Detailed Hello World Push integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.core import HomeAssistant 7 | from homeassistant.const import Platform 8 | 9 | from . import hub 10 | 11 | # List of platforms to support. There should be a matching .py file for each, 12 | # eg and 13 | PLATFORMS = [Platform.SENSOR, Platform.COVER] 14 | 15 | type HubConfigEntry = ConfigEntry[hub.Hub] 16 | 17 | 18 | async def async_setup_entry(hass: HomeAssistant, entry: HubConfigEntry) -> bool: 19 | """Set up Hello World from a config entry.""" 20 | # Store an instance of the "connecting" class that does the work of speaking 21 | # with your actual devices. 22 | entry.runtime_data = hub.Hub(hass, entry.data["host"]) 23 | 24 | # This creates each HA object for each platform your device requires. 25 | # It's done by calling the `async_setup_entry` function in each platform module. 26 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 27 | return True 28 | 29 | 30 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 31 | """Unload a config entry.""" 32 | # This is called when an entry/configured device is to be removed. The class 33 | # needs to unload itself, and remove callbacks. See the classes for further 34 | # details 35 | unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 36 | 37 | return unload_ok 38 | -------------------------------------------------------------------------------- /custom_components/example_load_platform/sensor.py: -------------------------------------------------------------------------------- 1 | """Platform for sensor integration.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.components.sensor import SensorEntity 5 | from homeassistant.const import UnitOfTemperature 6 | from homeassistant.core import HomeAssistant 7 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 8 | from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 9 | 10 | from . import DOMAIN 11 | 12 | 13 | def setup_platform( 14 | hass: HomeAssistant, 15 | config: ConfigType, 16 | add_entities: AddEntitiesCallback, 17 | discovery_info: DiscoveryInfoType | None = None 18 | ) -> None: 19 | """Set up the sensor platform.""" 20 | # We only want this platform to be set up via discovery. 21 | if discovery_info is None: 22 | return 23 | add_entities([ExampleSensor()]) 24 | 25 | 26 | class ExampleSensor(SensorEntity): 27 | """Representation of a sensor.""" 28 | 29 | def __init__(self) -> None: 30 | """Initialize the sensor.""" 31 | self._state = None 32 | 33 | @property 34 | def name(self) -> str: 35 | """Return the name of the sensor.""" 36 | return 'Example Temperature' 37 | 38 | @property 39 | def state(self): 40 | """Return the state of the sensor.""" 41 | return self._state 42 | 43 | @property 44 | def unit_of_measurement(self) -> str: 45 | """Return the unit of measurement.""" 46 | return UnitOfTemperature.CELSIUS 47 | 48 | def update(self) -> None: 49 | """Fetch new state data for the sensor. 50 | 51 | This is the only method that should fetch new data for Home Assistant. 52 | """ 53 | self._state = self.hass.data[DOMAIN]['temperature'] 54 | -------------------------------------------------------------------------------- /custom_components/mqtt_basic_sync/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of a custom MQTT component. 3 | 4 | Shows how to communicate with MQTT. Follows a topic on MQTT and updates the 5 | state of an entity to the last message received on that topic. 6 | 7 | Also offers a service 'set_state' that will publish a message on the topic that 8 | will be passed via MQTT to our message received listener. Call the service with 9 | example payload {"new_state": "some new state"}. 10 | 11 | Configuration: 12 | 13 | To use the mqtt_example component you will need to add the following to your 14 | configuration.yaml file. 15 | 16 | mqtt_basic_sync: 17 | topic: "home-assistant/mqtt_example" 18 | """ 19 | from __future__ import annotations 20 | 21 | import voluptuous as vol 22 | 23 | from homeassistant.components import mqtt 24 | from homeassistant.core import HomeAssistant, ServiceCall 25 | from homeassistant.helpers.typing import ConfigType 26 | 27 | # The domain of your component. Should be equal to the name of your component. 28 | DOMAIN = "mqtt_basic_sync" 29 | 30 | CONF_TOPIC = 'topic' 31 | DEFAULT_TOPIC = 'home-assistant/mqtt_example' 32 | 33 | # Schema to validate the configured MQTT topic 34 | CONFIG_SCHEMA = vol.Schema( 35 | { 36 | DOMAIN: vol.Schema( 37 | { 38 | vol.Optional( 39 | CONF_TOPIC, default=DEFAULT_TOPIC 40 | ): mqtt.valid_subscribe_topic 41 | } 42 | ) 43 | }, 44 | extra=vol.ALLOW_EXTRA, 45 | ) 46 | 47 | 48 | 49 | def setup(hass: HomeAssistant, config: ConfigType) -> bool: 50 | """Set up the MQTT example component.""" 51 | topic = config[DOMAIN][CONF_TOPIC] 52 | entity_id = 'mqtt_example.last_message' 53 | 54 | # Listen to a message on MQTT. 55 | def message_received(topic: str, payload: str, qos: int) -> None: 56 | """A new MQTT message has been received.""" 57 | hass.states.set(entity_id, payload) 58 | 59 | hass.components.mqtt.subscribe(topic, message_received) 60 | 61 | hass.states.set(entity_id, 'No messages') 62 | 63 | # Service to publish a message on MQTT. 64 | def set_state_service(call: ServiceCall) -> None: 65 | """Service to send a message.""" 66 | hass.components.mqtt.publish(topic, call.data.get('new_state')) 67 | 68 | # Register our service with Home Assistant. 69 | hass.services.register(DOMAIN, 'set_state', set_state_service) 70 | 71 | # Return boolean to indicate that initialization was successfully. 72 | return True 73 | -------------------------------------------------------------------------------- /custom_components/mqtt_basic_async/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example of a custom MQTT component. 3 | 4 | Shows how to communicate with MQTT. Follows a topic on MQTT and updates the 5 | state of an entity to the last message received on that topic. 6 | 7 | Also offers a service 'set_state' that will publish a message on the topic that 8 | will be passed via MQTT to our message received listener. Call the service with 9 | example payload {"new_state": "some new state"}. 10 | 11 | Configuration: 12 | 13 | To use the mqtt_example component you will need to add the following to your 14 | configuration.yaml file. 15 | 16 | mqtt_basic_async: 17 | topic: "home-assistant/mqtt_example" 18 | """ 19 | from __future__ import annotations 20 | 21 | import voluptuous as vol 22 | 23 | from homeassistant.components import mqtt 24 | from homeassistant.core import HomeAssistant, ServiceCall, callback 25 | from homeassistant.helpers.typing import ConfigType 26 | 27 | # The domain of your component. Should be equal to the name of your component. 28 | DOMAIN = "mqtt_basic_async" 29 | 30 | CONF_TOPIC = 'topic' 31 | DEFAULT_TOPIC = 'home-assistant/mqtt_example' 32 | 33 | # Schema to validate the configured MQTT topic 34 | CONFIG_SCHEMA = vol.Schema( 35 | { 36 | DOMAIN: vol.Schema( 37 | { 38 | vol.Optional( 39 | CONF_TOPIC, default=DEFAULT_TOPIC 40 | ): mqtt.valid_subscribe_topic 41 | } 42 | ) 43 | }, 44 | extra=vol.ALLOW_EXTRA, 45 | ) 46 | 47 | 48 | async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 49 | """Set up the MQTT async example component.""" 50 | topic = config[DOMAIN][CONF_TOPIC] 51 | entity_id = 'mqtt_example.last_message' 52 | 53 | # Listen to a message on MQTT. 54 | @callback 55 | def message_received(topic: str, payload: str, qos: int) -> None: 56 | """A new MQTT message has been received.""" 57 | hass.states.async_set(entity_id, payload) 58 | 59 | await hass.components.mqtt.async_subscribe(topic, message_received) 60 | 61 | hass.states.async_set(entity_id, 'No messages') 62 | 63 | # Service to publish a message on MQTT. 64 | @callback 65 | def set_state_service(call: ServiceCall) -> None: 66 | """Service to send a message.""" 67 | hass.components.mqtt.async_publish(topic, call.data.get('new_state')) 68 | 69 | # Register our service with Home Assistant. 70 | hass.services.async_register(DOMAIN, 'set_state', set_state_service) 71 | 72 | # Return boolean to indicate that initialization was successfully. 73 | return True 74 | -------------------------------------------------------------------------------- /custom_components/example_light/light.py: -------------------------------------------------------------------------------- 1 | """Platform for light integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | import awesomelights 7 | import voluptuous as vol 8 | 9 | # Import the device class from the component that you want to support 10 | import homeassistant.helpers.config_validation as cv 11 | from homeassistant.components.light import (ATTR_BRIGHTNESS, PLATFORM_SCHEMA, 12 | LightEntity) 13 | from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME 14 | from homeassistant.core import HomeAssistant 15 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 16 | from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | # Validation of the user's configuration 21 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 22 | vol.Required(CONF_HOST): cv.string, 23 | vol.Optional(CONF_USERNAME, default='admin'): cv.string, 24 | vol.Optional(CONF_PASSWORD): cv.string, 25 | }) 26 | 27 | 28 | def setup_platform( 29 | hass: HomeAssistant, 30 | config: ConfigType, 31 | add_entities: AddEntitiesCallback, 32 | discovery_info: DiscoveryInfoType | None = None 33 | ) -> None: 34 | """Set up the Awesome Light platform.""" 35 | # Assign configuration variables. 36 | # The configuration check takes care they are present. 37 | host = config[CONF_HOST] 38 | username = config[CONF_USERNAME] 39 | password = config.get(CONF_PASSWORD) 40 | 41 | # Setup connection with devices/cloud 42 | hub = awesomelights.Hub(host, username, password) 43 | 44 | # Verify that passed in configuration works 45 | if not hub.is_valid_login(): 46 | _LOGGER.error("Could not connect to AwesomeLight hub") 47 | return 48 | 49 | # Add devices 50 | add_entities(AwesomeLight(light) for light in hub.lights()) 51 | 52 | 53 | class AwesomeLight(LightEntity): 54 | """Representation of an Awesome Light.""" 55 | 56 | def __init__(self, light) -> None: 57 | """Initialize an AwesomeLight.""" 58 | self._light = light 59 | self._name = light.name 60 | self._state = None 61 | self._brightness = None 62 | 63 | @property 64 | def name(self) -> str: 65 | """Return the display name of this light.""" 66 | return self._name 67 | 68 | @property 69 | def brightness(self): 70 | """Return the brightness of the light. 71 | 72 | This method is optional. Removing it indicates to Home Assistant 73 | that brightness is not supported for this light. 74 | """ 75 | return self._brightness 76 | 77 | @property 78 | def is_on(self) -> bool | None: 79 | """Return true if light is on.""" 80 | return self._state 81 | 82 | def turn_on(self, **kwargs: Any) -> None: 83 | """Instruct the light to turn on. 84 | 85 | You can skip the brightness part if your light does not support 86 | brightness control. 87 | """ 88 | self._light.brightness = kwargs.get(ATTR_BRIGHTNESS, 255) 89 | self._light.turn_on() 90 | 91 | def turn_off(self, **kwargs: Any) -> None: 92 | """Instruct the light to turn off.""" 93 | self._light.turn_off() 94 | 95 | def update(self) -> None: 96 | """Fetch new state data for this light. 97 | 98 | This is the only method that should fetch new data for Home Assistant. 99 | """ 100 | self._light.update() 101 | self._state = self._light.is_on() 102 | self._brightness = self._light.brightness 103 | -------------------------------------------------------------------------------- /custom_components/detailed_hello_world_push/hub.py: -------------------------------------------------------------------------------- 1 | """A demonstration 'hub' that connects several devices.""" 2 | from __future__ import annotations 3 | 4 | # In a real implementation, this would be in an external library that's on PyPI. 5 | # The PyPI package needs to be included in the `requirements` section of manifest.json 6 | # See https://developers.home-assistant.io/docs/creating_integration_manifest 7 | # for more information. 8 | # This dummy hub always returns 3 rollers. 9 | import asyncio 10 | import random 11 | 12 | from homeassistant.core import HomeAssistant 13 | 14 | 15 | class Hub: 16 | """Dummy hub for Hello World example.""" 17 | 18 | manufacturer = "Demonstration Corp" 19 | 20 | def __init__(self, hass: HomeAssistant, host: str) -> None: 21 | """Init dummy hub.""" 22 | self._host = host 23 | self._hass = hass 24 | self._name = host 25 | self._id = host.lower() 26 | self.rollers = [ 27 | Roller(f"{self._id}_1", f"{self._name} 1", self), 28 | Roller(f"{self._id}_2", f"{self._name} 2", self), 29 | Roller(f"{self._id}_3", f"{self._name} 3", self), 30 | ] 31 | self.online = True 32 | 33 | @property 34 | def hub_id(self) -> str: 35 | """ID for dummy hub.""" 36 | return self._id 37 | 38 | async def test_connection(self) -> bool: 39 | """Test connectivity to the Dummy hub is OK.""" 40 | await asyncio.sleep(1) 41 | return True 42 | 43 | 44 | class Roller: 45 | """Dummy roller (device for HA) for Hello World example.""" 46 | 47 | def __init__(self, rollerid: str, name: str, hub: Hub) -> None: 48 | """Init dummy roller.""" 49 | self._id = rollerid 50 | self.hub = hub 51 | self.name = name 52 | self._callbacks = set() 53 | self._loop = asyncio.get_event_loop() 54 | self._target_position = 100 55 | self._current_position = 100 56 | # Reports if the roller is moving up or down. 57 | # >0 is up, <0 is down. This very much just for demonstration. 58 | self.moving = 0 59 | 60 | # Some static information about this device 61 | self.firmware_version = f"0.0.{random.randint(1, 9)}" 62 | self.model = "Test Device" 63 | 64 | @property 65 | def roller_id(self) -> str: 66 | """Return ID for roller.""" 67 | return self._id 68 | 69 | @property 70 | def position(self): 71 | """Return position for roller.""" 72 | return self._current_position 73 | 74 | async def set_position(self, position: int) -> None: 75 | """ 76 | Set dummy cover to the given position. 77 | 78 | State is announced a random number of seconds later. 79 | """ 80 | self._target_position = position 81 | 82 | # Update the moving status, and broadcast the update 83 | self.moving = position - 50 84 | await self.publish_updates() 85 | 86 | self._loop.create_task(self.delayed_update()) 87 | 88 | async def delayed_update(self) -> None: 89 | """Publish updates, with a random delay to emulate interaction with device.""" 90 | await asyncio.sleep(random.randint(1, 10)) 91 | self.moving = 0 92 | await self.publish_updates() 93 | 94 | def register_callback(self, callback: Callable[[], None]) -> None: 95 | """Register callback, called when Roller changes state.""" 96 | self._callbacks.add(callback) 97 | 98 | def remove_callback(self, callback: Callable[[], None]) -> None: 99 | """Remove previously registered callback.""" 100 | self._callbacks.discard(callback) 101 | 102 | # In a real implementation, this library would call it's call backs when it was 103 | # notified of any state changeds for the relevant device. 104 | async def publish_updates(self) -> None: 105 | """Schedule call all registered callbacks.""" 106 | self._current_position = self._target_position 107 | for callback in self._callbacks: 108 | callback() 109 | 110 | @property 111 | def online(self) -> float: 112 | """Roller is online.""" 113 | # The dummy roller is offline about 10% of the time. Returns True if online, 114 | # False if offline. 115 | return random.random() > 0.1 116 | 117 | @property 118 | def battery_level(self) -> int: 119 | """Battery level as a percentage.""" 120 | return random.randint(0, 100) 121 | 122 | @property 123 | def battery_voltage(self) -> float: 124 | """Return a random voltage roughly that of a 12v battery.""" 125 | return round(random.random() * 3 + 10, 2) 126 | 127 | @property 128 | def illuminance(self) -> int: 129 | """Return a sample illuminance in lux.""" 130 | return random.randint(0, 500) 131 | -------------------------------------------------------------------------------- /custom_components/detailed_hello_world_push/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Hello World integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | from typing import Any 6 | 7 | import voluptuous as vol 8 | 9 | from homeassistant import config_entries, exceptions 10 | from homeassistant.core import HomeAssistant 11 | 12 | from .const import DOMAIN # pylint:disable=unused-import 13 | from .hub import Hub 14 | 15 | _LOGGER = logging.getLogger(__name__) 16 | 17 | # This is the schema that used to display the UI to the user. This simple 18 | # schema has a single required host field, but it could include a number of fields 19 | # such as username, password etc. See other components in the HA core code for 20 | # further examples. 21 | # Note the input displayed to the user will be translated. See the 22 | # translations/.json file and strings.json. See here for further information: 23 | # https://developers.home-assistant.io/docs/config_entries_config_flow_handler/#translations 24 | # At the time of writing I found the translations created by the scaffold didn't 25 | # quite work as documented and always gave me the "Lokalise key references" string 26 | # (in square brackets), rather than the actual translated value. I did not attempt to 27 | # figure this out or look further into it. 28 | DATA_SCHEMA = vol.Schema({("host"): str}) 29 | 30 | 31 | async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: 32 | """Validate the user input allows us to connect. 33 | 34 | Data has the keys from DATA_SCHEMA with values provided by the user. 35 | """ 36 | # Validate the data can be used to set up a connection. 37 | 38 | # This is a simple example to show an error in the UI for a short hostname 39 | # The exceptions are defined at the end of this file, and are used in the 40 | # `async_step_user` method below. 41 | if len(data["host"]) < 3: 42 | raise InvalidHost 43 | 44 | hub = Hub(hass, data["host"]) 45 | # The dummy hub provides a `test_connection` method to ensure it's working 46 | # as expected 47 | result = await hub.test_connection() 48 | if not result: 49 | # If there is an error, raise an exception to notify HA that there was a 50 | # problem. The UI will also show there was a problem 51 | raise CannotConnect 52 | 53 | # If your PyPI package is not built with async, pass your methods 54 | # to the executor: 55 | # await hass.async_add_executor_job( 56 | # your_validate_func, data["username"], data["password"] 57 | # ) 58 | 59 | # If you cannot connect: 60 | # throw CannotConnect 61 | # If the authentication is wrong: 62 | # InvalidAuth 63 | 64 | # Return info that you want to store in the config entry. 65 | # "Title" is what is displayed to the user for this hub device 66 | # It is stored internally in HA as part of the device config. 67 | # See `async_step_user` below for how this is used 68 | return {"title": data["host"]} 69 | 70 | 71 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 72 | """Handle a config flow for Hello World.""" 73 | 74 | VERSION = 1 75 | # Pick one of the available connection classes in homeassistant/config_entries.py 76 | # This tells HA if it should be asking for updates, or it'll be notified of updates 77 | # automatically. This example uses PUSH, as the dummy hub will notify HA of 78 | # changes. 79 | CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH 80 | 81 | async def async_step_user(self, user_input=None): 82 | """Handle the initial step.""" 83 | # This goes through the steps to take the user through the setup process. 84 | # Using this it is possible to update the UI and prompt for additional 85 | # information. This example provides a single form (built from `DATA_SCHEMA`), 86 | # and when that has some validated input, it calls `async_create_entry` to 87 | # actually create the HA config entry. Note the "title" value is returned by 88 | # `validate_input` above. 89 | errors = {} 90 | if user_input is not None: 91 | try: 92 | info = await validate_input(self.hass, user_input) 93 | 94 | return self.async_create_entry(title=info["title"], data=user_input) 95 | except CannotConnect: 96 | errors["base"] = "cannot_connect" 97 | except InvalidHost: 98 | # The error string is set here, and should be translated. 99 | # This example does not currently cover translations, see the 100 | # comments on `DATA_SCHEMA` for further details. 101 | # Set the error on the `host` field, not the entire form. 102 | errors["host"] = "cannot_connect" 103 | except Exception: # pylint: disable=broad-except 104 | _LOGGER.exception("Unexpected exception") 105 | errors["base"] = "unknown" 106 | 107 | # If there is no user input or there were errors, show the form again, including any errors that were found with the input. 108 | return self.async_show_form( 109 | step_id="user", data_schema=DATA_SCHEMA, errors=errors 110 | ) 111 | 112 | 113 | class CannotConnect(exceptions.HomeAssistantError): 114 | """Error to indicate we cannot connect.""" 115 | 116 | 117 | class InvalidHost(exceptions.HomeAssistantError): 118 | """Error to indicate there is an invalid hostname.""" 119 | -------------------------------------------------------------------------------- /custom_components/detailed_hello_world_push/sensor.py: -------------------------------------------------------------------------------- 1 | """Platform for sensor integration.""" 2 | # This file shows the setup for the sensors associated with the cover. 3 | # They are setup in the same way with the call to the async_setup_entry function 4 | # via HA from the module __init__. Each sensor has a device_class, this tells HA how 5 | # to display it in the UI (for know types). The unit_of_measurement property tells HA 6 | # what the unit is, so it can display the correct range. For predefined types (such as 7 | # battery), the unit_of_measurement should match what's expected. 8 | import random 9 | 10 | from homeassistant.components.sensor import ( 11 | SensorDeviceClass, 12 | ) 13 | from homeassistant.const import ( 14 | PERCENTAGE, 15 | LIGHT_LUX, 16 | ) 17 | from homeassistant.core import HomeAssistant 18 | from homeassistant.helpers.entity import Entity 19 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 20 | 21 | from . import HubConfigEntry 22 | from .const import DOMAIN 23 | 24 | 25 | # See cover.py for more details. 26 | # Note how both entities for each roller sensor (battery and illuminance) are added at 27 | # the same time to the same list. This way only a single async_add_devices call is 28 | # required. 29 | async def async_setup_entry( 30 | hass: HomeAssistant, 31 | config_entry: HubConfigEntry, 32 | async_add_entities: AddEntitiesCallback, 33 | ) -> None: 34 | """Add sensors for passed config_entry in HA.""" 35 | hub = config_entry.runtime_data 36 | 37 | new_devices = [] 38 | for roller in hub.rollers: 39 | new_devices.append(BatterySensor(roller)) 40 | new_devices.append(IlluminanceSensor(roller)) 41 | if new_devices: 42 | async_add_entities(new_devices) 43 | 44 | 45 | # This base class shows the common properties and methods for a sensor as used in this 46 | # example. See each sensor for further details about properties and methods that 47 | # have been overridden. 48 | class SensorBase(Entity): 49 | """Base representation of a Hello World Sensor.""" 50 | 51 | should_poll = False 52 | 53 | def __init__(self, roller): 54 | """Initialize the sensor.""" 55 | self._roller = roller 56 | 57 | # To link this entity to the cover device, this property must return an 58 | # identifiers value matching that used in the cover, but no other information such 59 | # as name. If name is returned, this entity will then also become a device in the 60 | # HA UI. 61 | @property 62 | def device_info(self): 63 | """Return information to link this entity with the correct device.""" 64 | return {"identifiers": {(DOMAIN, self._roller.roller_id)}} 65 | 66 | # This property is important to let HA know if this entity is online or not. 67 | # If an entity is offline (return False), the UI will refelect this. 68 | @property 69 | def available(self) -> bool: 70 | """Return True if roller and hub is available.""" 71 | return self._roller.online and self._roller.hub.online 72 | 73 | async def async_added_to_hass(self): 74 | """Run when this Entity has been added to HA.""" 75 | # Sensors should also register callbacks to HA when their state changes 76 | self._roller.register_callback(self.async_write_ha_state) 77 | 78 | async def async_will_remove_from_hass(self): 79 | """Entity being removed from hass.""" 80 | # The opposite of async_added_to_hass. Remove any registered call backs here. 81 | self._roller.remove_callback(self.async_write_ha_state) 82 | 83 | 84 | class BatterySensor(SensorBase): 85 | """Representation of a Sensor.""" 86 | 87 | # The class of this device. Note the value should come from the homeassistant.const 88 | # module. More information on the available devices classes can be seen here: 89 | # https://developers.home-assistant.io/docs/core/entity/sensor 90 | device_class = SensorDeviceClass.BATTERY 91 | 92 | # The unit of measurement for this entity. As it's a DEVICE_CLASS_BATTERY, this 93 | # should be PERCENTAGE. A number of units are supported by HA, for some 94 | # examples, see: 95 | # https://developers.home-assistant.io/docs/core/entity/sensor#available-device-classes 96 | _attr_unit_of_measurement = PERCENTAGE 97 | 98 | def __init__(self, roller): 99 | """Initialize the sensor.""" 100 | super().__init__(roller) 101 | 102 | # As per the sensor, this must be a unique value within this domain. This is done 103 | # by using the device ID, and appending "_battery" 104 | self._attr_unique_id = f"{self._roller.roller_id}_battery" 105 | 106 | # The name of the entity 107 | self._attr_name = f"{self._roller.name} Battery" 108 | 109 | self._state = random.randint(0, 100) 110 | 111 | # The value of this sensor. As this is a DEVICE_CLASS_BATTERY, this value must be 112 | # the battery level as a percentage (between 0 and 100) 113 | @property 114 | def state(self): 115 | """Return the state of the sensor.""" 116 | return self._roller.battery_level 117 | 118 | 119 | # This is another sensor, but more simple compared to the battery above. See the 120 | # comments above for how each field works. 121 | class IlluminanceSensor(SensorBase): 122 | """Representation of a Sensor.""" 123 | 124 | device_class = SensorDeviceClass.ILLUMINANCE 125 | _attr_unit_of_measurement = LIGHT_LUX 126 | 127 | def __init__(self, roller): 128 | """Initialize the sensor.""" 129 | super().__init__(roller) 130 | # As per the sensor, this must be a unique value within this domain. This is done 131 | # by using the device ID, and appending "_battery" 132 | self._attr_unique_id = f"{self._roller.roller_id}_illuminance" 133 | 134 | # The name of the entity 135 | self._attr_name = f"{self._roller.name} Illuminance" 136 | 137 | @property 138 | def state(self): 139 | """Return the state of the sensor.""" 140 | return self._roller.illuminance 141 | -------------------------------------------------------------------------------- /custom_components/detailed_hello_world_push/cover.py: -------------------------------------------------------------------------------- 1 | """Platform for sensor integration.""" 2 | from __future__ import annotations 3 | 4 | from typing import Any 5 | 6 | # These constants are relevant to the type of entity we are using. 7 | # See below for how they are used. 8 | from homeassistant.components.cover import ( 9 | ATTR_POSITION, 10 | CoverEntityFeature, 11 | CoverEntity, 12 | ) 13 | from homeassistant.core import HomeAssistant 14 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 15 | 16 | from . import HubConfigEntry 17 | from .const import DOMAIN 18 | 19 | 20 | # This function is called as part of the __init__.async_setup_entry (via the 21 | # hass.config_entries.async_forward_entry_setup call) 22 | async def async_setup_entry( 23 | hass: HomeAssistant, 24 | config_entry: HubConfigEntry, 25 | async_add_entities: AddEntitiesCallback, 26 | ) -> None: 27 | """Add cover for passed config_entry in HA.""" 28 | # The hub is loaded from the associated entry runtime data that was set in the 29 | # __init__.async_setup_entry function 30 | hub = config_entry.runtime_data 31 | 32 | # Add all entities to HA 33 | async_add_entities(HelloWorldCover(roller) for roller in hub.rollers) 34 | 35 | 36 | # This entire class could be written to extend a base class to ensure common attributes 37 | # are kept identical/in sync. It's broken apart here between the Cover and Sensors to 38 | # be explicit about what is returned, and the comments outline where the overlap is. 39 | class HelloWorldCover(CoverEntity): 40 | """Representation of a dummy Cover.""" 41 | 42 | # Our dummy class is PUSH, so we tell HA that it should not be polled 43 | should_poll = False 44 | # The supported features of a cover are done using a bitmask. Using the constants 45 | # imported above, we can tell HA the features that are supported by this entity. 46 | # If the supported features were dynamic (ie: different depending on the external 47 | # device it connected to), then this should be function with an @property decorator. 48 | supported_features = CoverEntityFeature.SET_POSITION | CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE 49 | 50 | def __init__(self, roller) -> None: 51 | """Initialize the sensor.""" 52 | # Usual setup is done here. Callbacks are added in async_added_to_hass. 53 | self._roller = roller 54 | 55 | # A unique_id for this entity with in this domain. This means for example if you 56 | # have a sensor on this cover, you must ensure the value returned is unique, 57 | # which is done here by appending "_cover". For more information, see: 58 | # https://developers.home-assistant.io/docs/entity_registry_index/#unique-id-requirements 59 | # Note: This is NOT used to generate the user visible Entity ID used in automations. 60 | self._attr_unique_id = f"{self._roller.roller_id}_cover" 61 | 62 | # This is the name for this *entity*, the "name" attribute from "device_info" 63 | # is used as the device name for device screens in the UI. This name is used on 64 | # entity screens, and used to build the Entity ID that's used is automations etc. 65 | self._attr_name = self._roller.name 66 | 67 | async def async_added_to_hass(self) -> None: 68 | """Run when this Entity has been added to HA.""" 69 | # Importantly for a push integration, the module that will be getting updates 70 | # needs to notify HA of changes. The dummy device has a registercallback 71 | # method, so to this we add the 'self.async_write_ha_state' method, to be 72 | # called where ever there are changes. 73 | # The call back registration is done once this entity is registered with HA 74 | # (rather than in the __init__) 75 | self._roller.register_callback(self.async_write_ha_state) 76 | 77 | async def async_will_remove_from_hass(self) -> None: 78 | """Entity being removed from hass.""" 79 | # The opposite of async_added_to_hass. Remove any registered call backs here. 80 | self._roller.remove_callback(self.async_write_ha_state) 81 | 82 | # Information about the devices that is partially visible in the UI. 83 | # The most critical thing here is to give this entity a name so it is displayed 84 | # as a "device" in the HA UI. This name is used on the Devices overview table, 85 | # and the initial screen when the device is added (rather than the entity name 86 | # property below). You can then associate other Entities (eg: a battery 87 | # sensor) with this device, so it shows more like a unified element in the UI. 88 | # For example, an associated battery sensor will be displayed in the right most 89 | # column in the Configuration > Devices view for a device. 90 | # To associate an entity with this device, the device_info must also return an 91 | # identical "identifiers" attribute, but not return a name attribute. 92 | # See the sensors.py file for the corresponding example setup. 93 | # Additional meta data can also be returned here, including sw_version (displayed 94 | # as Firmware), model and manufacturer (displayed as by ) 95 | # shown on the device info screen. The Manufacturer and model also have their 96 | # respective columns on the Devices overview table. Note: Many of these must be 97 | # set when the device is first added, and they are not always automatically 98 | # refreshed by HA from it's internal cache. 99 | # For more information see: 100 | # https://developers.home-assistant.io/docs/device_registry_index/#device-properties 101 | @property 102 | def device_info(self) -> DeviceInfo: 103 | """Information about this entity/device.""" 104 | return { 105 | "identifiers": {(DOMAIN, self._roller.roller_id)}, 106 | # If desired, the name for the device could be different to the entity 107 | "name": self.name, 108 | "sw_version": self._roller.firmware_version, 109 | "model": self._roller.model, 110 | "manufacturer": self._roller.hub.manufacturer, 111 | } 112 | 113 | # This property is important to let HA know if this entity is online or not. 114 | # If an entity is offline (return False), the UI will refelect this. 115 | @property 116 | def available(self) -> bool: 117 | """Return True if roller and hub is available.""" 118 | return self._roller.online and self._roller.hub.online 119 | 120 | # The following properties are how HA knows the current state of the device. 121 | # These must return a value from memory, not make a live query to the device/hub 122 | # etc when called (hence they are properties). For a push based integration, 123 | # HA is notified of changes via the async_write_ha_state call. See the __init__ 124 | # method for hos this is implemented in this example. 125 | # The properties that are expected for a cover are based on the supported_features 126 | # property of the object. In the case of a cover, see the following for more 127 | # details: https://developers.home-assistant.io/docs/core/entity/cover/ 128 | @property 129 | def current_cover_position(self): 130 | """Return the current position of the cover.""" 131 | return self._roller.position 132 | 133 | @property 134 | def is_closed(self) -> bool: 135 | """Return if the cover is closed, same as position 0.""" 136 | return self._roller.position == 0 137 | 138 | @property 139 | def is_closing(self) -> bool: 140 | """Return if the cover is closing or not.""" 141 | return self._roller.moving < 0 142 | 143 | @property 144 | def is_opening(self) -> bool: 145 | """Return if the cover is opening or not.""" 146 | return self._roller.moving > 0 147 | 148 | # These methods allow HA to tell the actual device what to do. In this case, move 149 | # the cover to the desired position, or open and close it all the way. 150 | async def async_open_cover(self, **kwargs: Any) -> None: 151 | """Open the cover.""" 152 | await self._roller.set_position(100) 153 | 154 | async def async_close_cover(self, **kwargs: Any) -> None: 155 | """Close the cover.""" 156 | await self._roller.set_position(0) 157 | 158 | async def async_set_cover_position(self, **kwargs: Any) -> None: 159 | """Close the cover.""" 160 | await self._roller.set_position(kwargs[ATTR_POSITION]) 161 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------