diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 9c65fef..a41df46 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -22,7 +22,7 @@ body: required: true - label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue). required: true - - label: This issue is not a duplicate issue of currently [previous issues](https://github.com/ludeeus/integration_blueprint/issues?q=is%3Aissue+label%3A%22Bug%22+).. + - label: This issue is not a duplicate issue of currently [previous issues](https://github.com/wrodie/ha_behringer_mixer/issues?q=is%3Aissue+label%3A%22Bug%22+).. required: true - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 433467b..cb4aa69 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -14,7 +14,7 @@ body: required: true - label: This only contains 1 feature request (if you have multiple feature requests, open one feature request for each feature request). required: true - - label: This issue is not a duplicate feature request of [previous feature requests](https://github.com/ludeeus/integration_blueprint/issues?q=is%3Aissue+label%3A%22Feature+Request%22+). + - label: This issue is not a duplicate feature request of [previous feature requests](https://github.com/wrodie/ha_behringer_mixer/issues?q=is%3Aissue+label%3A%22Feature+Request%22+). required: true - type: textarea diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 28f9fce..d6f0ccd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,10 +14,10 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout the repository" - uses: "actions/checkout@v3.5.2" + uses: "actions/checkout@v4.1.1" - name: "Set up Python" - uses: actions/setup-python@v4.6.1 + uses: actions/setup-python@v4.7.1 with: python-version: "3.10" cache: "pip" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f2ac43a..98202a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,21 +15,21 @@ jobs: contents: write steps: - name: "Checkout the repository" - uses: "actions/checkout@v3.5.2" + uses: "actions/checkout@v4.1.1" - name: "Adjust version number" shell: "bash" run: | yq -i -o json '.version="${{ github.event.release.tag_name }}"' \ - "${{ github.workspace }}/custom_components/integration_blueprint/manifest.json" + "${{ github.workspace }}/custom_components/ha_behringer_mixer/manifest.json" - name: "ZIP the integration directory" shell: "bash" run: | - cd "${{ github.workspace }}/custom_components/integration_blueprint" - zip integration_blueprint.zip -r ./ + cd "${{ github.workspace }}/custom_components/ha_behringer_mixer" + zip ha_behringer_mixer.zip -r ./ - name: "Upload the ZIP file to the release" uses: softprops/action-gh-release@v0.1.15 with: - files: ${{ github.workspace }}/custom_components/integration_blueprint/integration_blueprint.zip + files: ${{ github.workspace }}/custom_components/ha_behringer_mixer/ha_behringer_mixer.zip diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6d257aa..502f5dd 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,7 +17,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout the repository" - uses: "actions/checkout@v3.5.2" + uses: "actions/checkout@v4.1.1" - name: "Run hassfest validation" uses: "home-assistant/actions/hassfest@master" @@ -27,7 +27,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: "Checkout the repository" - uses: "actions/checkout@v3.5.2" + uses: "actions/checkout@v4.1.1" - name: "Run HACS validation" uses: "hacs/action@main" diff --git a/README.md b/README.md index e15e9ef..ee1655e 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,83 @@ -# Notice - -The component and platforms in this repository are not meant to be used by a -user, but as a "blueprint" that custom component developers can build -upon, to make more awesome stuff. - -HAVE FUN! 😎 - -## Why? - -This is simple, by having custom_components look (README + structure) the same -it is easier for developers to help each other and for users to start using them. - -If you are a developer and you want to add things to this "blueprint" that you think more -developers will have use for, please open a PR to add it :) - -## What? - -This repository contains multiple files, here is a overview: - -File | Purpose | Documentation --- | -- | -- -`.devcontainer.json` | Used for development/testing with Visual Studio Code. | [Documentation](https://code.visualstudio.com/docs/remote/containers) -`.github/ISSUE_TEMPLATE/*.yml` | Templates for the issue tracker | [Documentation](https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository) -`.vscode/tasks.json` | Tasks for the devcontainer. | [Documentation](https://code.visualstudio.com/docs/editor/tasks) -`custom_components/integration_blueprint/*` | Integration files, this is where everything happens. | [Documentation](https://developers.home-assistant.io/docs/creating_component_index) -`CONTRIBUTING.md` | Guidelines on how to contribute. | [Documentation](https://help.github.com/en/github/building-a-strong-community/setting-guidelines-for-repository-contributors) -`LICENSE` | The license file for the project. | [Documentation](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/licensing-a-repository) -`README.md` | The file you are reading now, should contain info about the integration, installation and configuration instructions. | [Documentation](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax) -`requirements.txt` | Python packages used for development/lint/testing this integration. | [Documentation](https://pip.pypa.io/en/stable/user_guide/#requirements-files) - -## How? - -1. Create a new repository in GitHub, using this repository as a template by clicking the "Use this template" button in the GitHub UI. -1. Open your new repository in Visual Studio Code devcontainer (Preferably with the "`Dev Containers: Clone Repository in Named Container Volume...`" option). -1. Rename all instances of the `integration_blueprint` to `custom_components/` (e.g. `custom_components/awesome_integration`). -1. Rename all instances of the `Integration Blueprint` to `` (e.g. `Awesome Integration`). -1. Run the `scripts/develop` to start HA and test out your new integration. - -## Next steps - -These are some next steps you may want to look into: -- Add tests to your integration, [`pytest-homeassistant-custom-component`](https://github.com/MatthewFlamm/pytest-homeassistant-custom-component) can help you get started. -- Add brand images (logo/icon) to https://github.com/home-assistant/brands. -- Create your first release. -- Share your integration on the [Home Assistant Forum](https://community.home-assistant.io/). -- Submit your integration to the [HACS](https://hacs.xyz/docs/publish/start). +# Behringer Digital Mixer Integration For Home Assistant + +[![GitHub Release][releases-shield]][releases] +[![GitHub Activity][commits-shield]][commits] +[![License][license-shield]](LICENSE) + +[![hacs][hacsbadge]][hacs] +![Project Maintenance][maintenance-shield] + + +This integration allows you to connect a Behringer Digital Mixer to Home Assistant. + +**Mixers Supported** +- X32 +- XR12 +- XR16 +- XR18 + +*Testing has been mostly on the X32* + +For each mixer configured by this integration entities for the following are provided.: + +For each Channel, Bus, DCA, Matrix, and Main Faders + - Name (Read only) + - Mute (SWITCH) (Read/Write) + - Fader (NUMBER) (Read/Write) + - Fader Value - dB (SENSOR) (Read only) + +In addition to these 'fader' related variables, also provided is + - Current scene/snapshot number/index (Read/Write) + +## Data Updates +The data for the mixer is updated in real time, so each time a button is pressed or fader is moved on the mixer, this is updated in Home Assistant immediately. + + + + +## Installation + +**HACS installation (recommended)** + +1. Install HACS. That way you get updates automatically. +1. Add this Github repository as custom repository in HACS settings. +1. Aearch and install "Behringer Mixer" in HACS and click install. +1. Restart Home Assistant, +1. Then you can add a Behringer Mixer integration in the integration page. + +**Manual installation** + +1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). +1. If you do not have a `custom_components` directory (folder) there, you need to create it. +1. In the `custom_components` directory (folder) create a new folder called `ha_berhringer_mixer`. +1. Download _all_ the files from the `custom_components/ha_berhringer_mixer/` directory (folder) in this repository. +1. Place the files you downloaded in the new directory (folder) you created. +1. Restart Home Assistant +1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Behringer Mixer" + + + +## Configuration is done in the UI + + + +# Caveats +Connection to the mixer is performed via ip address using UDP. If the IP address for the mixer changes, you will need to edit the integration setup. To avoid this, set up a DHCP reservation on your router for your mixer so that it always has the same IP address. + +This information on changes to the mixer is written to the HA history/recorder databases so this may result in lots of state being stored if the mixer changes a lot. You may want to consider excluding these entities from storing history. + + + + +## Contributions are welcome! + +If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md) + +*** + + +[commits]: https://github.com/wrodie/ha_behringer_mixer/commits/main +[hacs]: https://github.com/hacs/integration +[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge +[releases-shield]: https://img.shields.io/github/release/wrodie/ha_behringer_mixer.svg?style=for-the-badge +[releases]: https://github.com/wrodie/ha_behringer_mixer/releases diff --git a/README_EXAMPLE.md b/README_EXAMPLE.md deleted file mode 100644 index 566d4dd..0000000 --- a/README_EXAMPLE.md +++ /dev/null @@ -1,59 +0,0 @@ -# Integration Blueprint - -[![GitHub Release][releases-shield]][releases] -[![GitHub Activity][commits-shield]][commits] -[![License][license-shield]](LICENSE) - -[![hacs][hacsbadge]][hacs] -![Project Maintenance][maintenance-shield] -[![BuyMeCoffee][buymecoffeebadge]][buymecoffee] - -[![Discord][discord-shield]][discord] -[![Community Forum][forum-shield]][forum] - -_Integration to integrate with [integration_blueprint][integration_blueprint]._ - -**This integration will set up the following platforms.** - -Platform | Description --- | -- -`binary_sensor` | Show something `True` or `False`. -`sensor` | Show info from blueprint API. -`switch` | Switch something `True` or `False`. - -## Installation - -1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`). -1. If you do not have a `custom_components` directory (folder) there, you need to create it. -1. In the `custom_components` directory (folder) create a new folder called `integration_blueprint`. -1. Download _all_ the files from the `custom_components/integration_blueprint/` directory (folder) in this repository. -1. Place the files you downloaded in the new directory (folder) you created. -1. Restart Home Assistant -1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Integration blueprint" - -## Configuration is done in the UI - - - -## Contributions are welcome! - -If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md) - -*** - -[integration_blueprint]: https://github.com/ludeeus/integration_blueprint -[buymecoffee]: https://www.buymeacoffee.com/ludeeus -[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge -[commits-shield]: https://img.shields.io/github/commit-activity/y/ludeeus/integration_blueprint.svg?style=for-the-badge -[commits]: https://github.com/ludeeus/integration_blueprint/commits/main -[hacs]: https://github.com/hacs/integration -[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge -[discord]: https://discord.gg/Qa5fW2R -[discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge -[exampleimg]: example.png -[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge -[forum]: https://community.home-assistant.io/ -[license-shield]: https://img.shields.io/github/license/ludeeus/integration_blueprint.svg?style=for-the-badge -[maintenance-shield]: https://img.shields.io/badge/maintainer-Joakim%20Sørensen%20%40ludeeus-blue.svg?style=for-the-badge -[releases-shield]: https://img.shields.io/github/release/ludeeus/integration_blueprint.svg?style=for-the-badge -[releases]: https://github.com/ludeeus/integration_blueprint/releases diff --git a/config/configuration.yaml b/config/configuration.yaml index 8c0d4e4..a3d2af9 100644 --- a/config/configuration.yaml +++ b/config/configuration.yaml @@ -5,4 +5,4 @@ default_config: logger: default: info logs: - custom_components.integration_blueprint: debug + custom_components.ha_behringer_mixer: debug diff --git a/custom_components/integration_blueprint/__init__.py b/custom_components/ha_behringer_mixer/__init__.py similarity index 50% rename from custom_components/integration_blueprint/__init__.py rename to custom_components/ha_behringer_mixer/__init__.py index a9adfdc..4082caf 100644 --- a/custom_components/integration_blueprint/__init__.py +++ b/custom_components/ha_behringer_mixer/__init__.py @@ -1,50 +1,52 @@ -"""Custom integration to integrate integration_blueprint with Home Assistant. - -For more details about this integration, please refer to -https://github.com/ludeeus/integration_blueprint -""" +"""Custom integration to integrate a Behringer mixer into Home Assistant.""" from __future__ import annotations -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .api import IntegrationBlueprintApiClient +from .api import BehringerMixerApiClient from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator +from .coordinator import MixerDataUpdateCoordinator PLATFORMS: list[Platform] = [ - Platform.SENSOR, - Platform.BINARY_SENSOR, Platform.SWITCH, + Platform.NUMBER, + Platform.SENSOR, ] -# https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up this integration using UI.""" hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator = BlueprintDataUpdateCoordinator( + + client = BehringerMixerApiClient( + mixer_ip=entry.data["MIXER_IP"], mixer_type=entry.data["MIXER_TYPE"] + ) + if not await client.setup(): + raise ConfigEntryNotReady( + f"Timeout while connecting to {entry.data['MIXER_IP']}" + ) + + hass.data[DOMAIN][entry.entry_id] = coordinator = MixerDataUpdateCoordinator( hass=hass, - client=IntegrationBlueprintApiClient( - username=entry.data[CONF_USERNAME], - password=entry.data[CONF_PASSWORD], - session=async_get_clientsession(hass), - ), + client=client, ) - # https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities await coordinator.async_config_entry_first_refresh() + client.register_coordinator(coordinator) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle removal of an entry.""" if unloaded := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN][entry.entry_id].client.stop() hass.data[DOMAIN].pop(entry.entry_id) return unloaded diff --git a/custom_components/ha_behringer_mixer/api.py b/custom_components/ha_behringer_mixer/api.py new file mode 100644 index 0000000..c881023 --- /dev/null +++ b/custom_components/ha_behringer_mixer/api.py @@ -0,0 +1,75 @@ +"""API Client. to connect to Behringer mixer.""" +from __future__ import annotations +import logging +import asyncio +from behringer_mixer import mixer_api + + +class BehringerMixerApiClientError(Exception): + """Exception to indicate a general API error.""" + + +class BehringerMixerApiClientCommunicationError(BehringerMixerApiClientError): + """Exception to indicate a communication error.""" + + +class BehringerMixerApiClientAuthenticationError(BehringerMixerApiClientError): + """Exception to indicate an authentication error.""" + + +class BehringerMixerApiClient: + """Behringer Mixer API Client.""" + + def __init__(self, mixer_ip: str, mixer_type: str) -> None: + """Initialise the API Client.""" + self._mixer_ip = mixer_ip + self._mixer_type = mixer_type + self._state = {} + self._mixer = None + self.tasks = set() + self.coordinator = None + + async def setup(self): + """Set up everything necessary.""" + self._mixer = mixer_api.create( + self._mixer_type, ip=self._mixer_ip, logLevel=logging.WARNING, delay=0.002 + ) + await self._mixer.start() + # Get Initial state first + await self._mixer.reload() + # Setup subscription for live updates + task = asyncio.create_task(self._mixer.subscribe(self.new_data_callback)) + self.tasks.add(task) + task.add_done_callback(self.tasks.discard) + return True + + def mixer_info(self): + """Return the mixer info.""" + return self._mixer.info() + + async def async_get_data(self) -> any: + """Get data from the API.""" + return self._mixer.state() + + async def async_set_value(self, address: str, value: str) -> any: + """Set a specific value on the mixer.""" + return await self._mixer.set_value(address, value) + + async def load_scene(self, scene_number): + """Change the scene.""" + return await self._mixer.load_scene(scene_number) + + def new_data_callback(self, data: dict): # pylint: disable=unused-argument + """Callback function that receives new data from the mixer.""" + if self.coordinator: + self.coordinator.async_update_listeners() + return True + + def register_coordinator(self, coordinator): + """Register the coordinator object.""" + self.coordinator = coordinator + + def stop(self): + """Shutdown the client.""" + self._mixer.unsubscribe() + self._mixer.stop() diff --git a/custom_components/ha_behringer_mixer/config_flow.py b/custom_components/ha_behringer_mixer/config_flow.py new file mode 100644 index 0000000..d9ea30c --- /dev/null +++ b/custom_components/ha_behringer_mixer/config_flow.py @@ -0,0 +1,67 @@ +"""Adds config flow for BehringerMixer.""" +from __future__ import annotations + +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.helpers import selector + +from .api import ( + BehringerMixerApiClient, + BehringerMixerApiClientAuthenticationError, + BehringerMixerApiClientCommunicationError, + BehringerMixerApiClientError, +) +from .const import DOMAIN, LOGGER + + +class BehringerMixerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for BehringerMixer.""" + + VERSION = 1 + + async def async_step_user( + self, + user_input: dict | None = None, + ) -> config_entries.FlowResult: + """Handle a flow initialized by the user.""" + _errors = {} + if user_input is not None: + try: + await self._test_connect( + mixer_ip=user_input["MIXER_IP"], + mixer_type=user_input["MIXER_TYPE"], + ) + except BehringerMixerApiClientAuthenticationError as exception: + LOGGER.warning(exception) + _errors["base"] = "auth" + except BehringerMixerApiClientCommunicationError as exception: + LOGGER.error(exception) + _errors["base"] = "connection" + except BehringerMixerApiClientError as exception: + LOGGER.exception(exception) + _errors["base"] = "unknown" + else: + return self.async_create_entry( + title=user_input["MIXER_IP"], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required("MIXER_IP"): selector.TextSelector( + selector.TextSelectorConfig( + type=selector.TextSelectorType.TEXT + ), + ), + vol.Required("MIXER_TYPE", default="X32"): vol.In(["X32", "XR12", "XR16", "XR18"]), + } + ), + errors=_errors, + ) + + async def _test_connect(self, mixer_ip: str, mixer_type: str) -> None: + """Validate credentials.""" + client = BehringerMixerApiClient(mixer_ip=mixer_ip, mixer_type=mixer_type) + await client.async_get_data() diff --git a/custom_components/integration_blueprint/const.py b/custom_components/ha_behringer_mixer/const.py similarity index 70% rename from custom_components/integration_blueprint/const.py rename to custom_components/ha_behringer_mixer/const.py index 66c28f3..04307e8 100644 --- a/custom_components/integration_blueprint/const.py +++ b/custom_components/ha_behringer_mixer/const.py @@ -3,7 +3,7 @@ LOGGER: Logger = getLogger(__package__) -NAME = "Integration blueprint" -DOMAIN = "integration_blueprint" -VERSION = "0.0.0" +NAME = "Behringer Mixer" +DOMAIN = "ha_behringer_mixer" +VERSION = "0.0.1" ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/" diff --git a/custom_components/ha_behringer_mixer/coordinator.py b/custom_components/ha_behringer_mixer/coordinator.py new file mode 100644 index 0000000..bcd3800 --- /dev/null +++ b/custom_components/ha_behringer_mixer/coordinator.py @@ -0,0 +1,108 @@ +"""DataUpdateCoordinator for Behringer Mixer Integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator, + UpdateFailed, +) +from homeassistant.exceptions import ConfigEntryAuthFailed + +from .api import ( + BehringerMixerApiClient, + BehringerMixerApiClientAuthenticationError, + BehringerMixerApiClientError, +) +from .const import DOMAIN, LOGGER + + +class MixerDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching data from the API.""" + + config_entry: ConfigEntry + entity_catalog: {} + + def __init__( + self, + hass: HomeAssistant, + client: BehringerMixerApiClient, + ) -> None: + """Initialize.""" + self.client = client + super().__init__( + hass=hass, + logger=LOGGER, + name=DOMAIN, + ) + self.entity_catalog = self.build_entity_catalog(self.client.mixer_info()) + + async def _async_update_data(self): + """Update data via library.""" + try: + return await self.client.async_get_data() + except BehringerMixerApiClientAuthenticationError as exception: + raise ConfigEntryAuthFailed(exception) from exception + except BehringerMixerApiClientError as exception: + raise UpdateFailed(exception) from exception + + def build_entity_catalog(self, mixer_info): + """Build a list of entities.""" + types = ["channel", "bus", "dca", "matrix"] + entities = { + "SENSOR": [], + "NUMBER": [], + "SWITCH": [], + } + self.fader_group(entities, "main", 0, "main/st") + for entity_type in types: + num_type = mixer_info.get(entity_type, {}).get("number") + base_key = mixer_info.get(entity_type, {}).get("base_address") + for index_number in range(1, num_type + 1): + self.fader_group(entities, entity_type, index_number, base_key) + entities["NUMBER"].append( + { + "type": "scene", + "key": f"{self.config_entry.entry_id}_scene_current", + "default_name": "Current Scene", + "base_address": "/scene/current", + } + ) + return entities + + def fader_group(self, entities, entity_type, index_number, base_key): + """Generate entities for a fader.""" + entity_part = entity_type + base_address = f"/{base_key}" + default_name = entity_type + if index_number: + entity_part = entity_type + "_" + str(index_number) + base_address = base_address + "/" + str(index_number) + default_name = default_name + " " + str(index_number or 0) + entities["SWITCH"].append( + { + "type": "on", + "key": f"{self.config_entry.entry_id}_{entity_part}_on", + "default_name": default_name, + "name_suffix": "On", + "base_address": base_address, + } + ) + entities["NUMBER"].append( + { + "type": "fader", + "key": f"{self.config_entry.entry_id}_{entity_part}_fader", + "default_name": default_name, + "name_suffix": "Fader", + "base_address": base_address, + } + ) + entities["SENSOR"].append( + { + "type": "faderdb", + "key": f"{self.config_entry.entry_id}_{entity_part}_fader_db", + "default_name": default_name, + "name_suffix": "Fader (dB)", + "base_address": base_address, + } + ) diff --git a/custom_components/ha_behringer_mixer/entity.py b/custom_components/ha_behringer_mixer/entity.py new file mode 100644 index 0000000..32036b2 --- /dev/null +++ b/custom_components/ha_behringer_mixer/entity.py @@ -0,0 +1,50 @@ +"""BlueprintEntity class.""" +from __future__ import annotations + +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTRIBUTION, DOMAIN, VERSION +from .coordinator import MixerDataUpdateCoordinator + + +class BehringerMixerEntity(CoordinatorEntity): + """BlueprintEntity class.""" + + _attr_attribution = ATTRIBUTION + _attr_should_poll = False + + def __init__( + self, + coordinator: MixerDataUpdateCoordinator, + entity_description: EntityDescription, + entity_setup: dict, + ) -> None: + """Initialize the entity class.""" + super().__init__(coordinator) + self.base_address = entity_setup.get("base_address") + self.default_name = entity_setup.get("default_name") + self.name_suffix = entity_setup.get("name_suffix") + key = entity_setup.get("key") + self._attr_unique_id = key + self._attr_entity_id = DOMAIN + "." + key + self.entity_id = self._attr_entity_id + self.entity_description = entity_description + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + name=f"Mixer - {coordinator.config_entry.data['MIXER_TYPE']} - {coordinator.config_entry.data['MIXER_IP']}", + model=VERSION, + manufacturer="Behringer", + ) + + @property + def name(self) -> str | None: + """Name of the entity.""" + return ( + ( + self.coordinator.data.get(self.base_address + "/config_name", "") + or self.default_name + ) + + " " + + self.name_suffix + ) diff --git a/custom_components/ha_behringer_mixer/manifest.json b/custom_components/ha_behringer_mixer/manifest.json new file mode 100644 index 0000000..8be8f56 --- /dev/null +++ b/custom_components/ha_behringer_mixer/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "ha_behringer_mixer", + "name": "Behringer Mixer", + "codeowners": [ + "@wrodie" + ], + "config_flow": true, + "documentation": "https://github.com/wrodie/ha_behringer_mixer", + "integration_type": "device", + "iot_class": "local_push", + "issue_tracker": "https://github.com/wrodie/ha_behringer_mixer/issues", + "requirements": [ + "behringer-mixer" + ], + "version": "0.1.0" +} \ No newline at end of file diff --git a/custom_components/ha_behringer_mixer/number.py b/custom_components/ha_behringer_mixer/number.py new file mode 100644 index 0000000..29e9255 --- /dev/null +++ b/custom_components/ha_behringer_mixer/number.py @@ -0,0 +1,95 @@ +"""Number platform for behringer_mixer.""" +from __future__ import annotations + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.helpers import config_validation as cv, entity_platform +import voluptuous as vol + +from .const import DOMAIN +from .entity import BehringerMixerEntity + + +async def async_setup_entry(hass, entry, async_add_devices): + """Set up the sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + devices_list = build_entities(coordinator) + async_add_devices(devices_list) + + # Register service to change scenes + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + "SERVICE_CHANGE_SCENE", + { + vol.Required("scene_number"): cv.Number, + }, + "change_scene", + ) + + +def build_entities(coordinator): + """Build up the entities.""" + entities = [] + for entity in coordinator.entity_catalog.get("NUMBER"): + if entity.get("type") == "scene": + entities.append( + BehringerMixerSceneNumber( + coordinator=coordinator, + entity_description=NumberEntityDescription( + key=entity.get("key"), + name=entity.get("default_name"), + ), + entity_setup=entity, + ) + ) + else: + entities.append( + BehringerMixerFader( + coordinator=coordinator, + entity_description=NumberEntityDescription( + key=entity.get("key"), + name=entity.get("default_name"), + ), + entity_setup=entity, + ) + ) + return entities + + +class BehringerMixerSceneNumber(BehringerMixerEntity, NumberEntity): + """Behringer_mixer Scene Number class.""" + + _attr_mode = "box" + + @property + def native_value(self) -> float | None: + """Value of the entity.""" + return self.coordinator.data.get(self.base_address, "") + + @property + def name(self) -> str | None: + """Name of the entity.""" + return self.default_name + + async def async_set_native_value(self, value: float) -> None: + """Update the current scene.""" + await self.coordinator.client.load_scene(int(value)) + + +class BehringerMixerFader(BehringerMixerEntity, NumberEntity): + """Behringer_mixer Number class.""" + + _attr_native_max_value = 1 + _attr_native_min_value = 0 + _attr_icon = "mdi:volume-source" + + @property + def native_value(self) -> float | None: + """Value of the entity.""" + return self.coordinator.data.get(self.base_address + "/mix_fader", "") + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + + await self.coordinator.client.async_set_value( + self.base_address + "/mix_fader", value + ) diff --git a/custom_components/ha_behringer_mixer/sensor.py b/custom_components/ha_behringer_mixer/sensor.py new file mode 100644 index 0000000..20463ce --- /dev/null +++ b/custom_components/ha_behringer_mixer/sensor.py @@ -0,0 +1,44 @@ +"""Sensor platform for behringer_mixer.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription + +from .const import DOMAIN +from .entity import BehringerMixerEntity + + +async def async_setup_entry(hass, entry, async_add_devices): + """Set up the sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + devices_list = build_entities(coordinator) + async_add_devices(devices_list) + + +def build_entities(coordinator): + """Build up the entities.""" + entities = [] + for entity in coordinator.entity_catalog.get("SENSOR"): + entities.append( + BehringerMixerSensor( + coordinator=coordinator, + entity_description=SensorEntityDescription( + key=entity.get("key"), + name=entity.get("default_name"), + ), + entity_setup=entity, + ) + ) + return entities + + +class BehringerMixerSensor(BehringerMixerEntity, SensorEntity): + """Behringer_mixer Sensor class.""" + + _attr_device_class = "SensorDeviceClass.SOUND_PRESSURE" + _attr_native_unit_of_measurement = "dB" + _attr_icon = "mdi:volume-source" + + @property + def native_value(self) -> float | None: + """Value of the entity.""" + return self.coordinator.data.get(self.base_address + "/mix_fader_db", "") or -90 diff --git a/custom_components/ha_behringer_mixer/services.yaml b/custom_components/ha_behringer_mixer/services.yaml new file mode 100644 index 0000000..c9db424 --- /dev/null +++ b/custom_components/ha_behringer_mixer/services.yaml @@ -0,0 +1,19 @@ +change_scene: + name: Change Scene + description: Change the scene/snapshot on the mixer + target: + entity: + # integration: ha_behringer_mixer + fields: + scene_number: + required: true + selector: + text: + #number: + # min: 0 + # max: 100 + #entity_id: + # required: true + # selector: + # entity: + # domain: ha_behringer_mixer diff --git a/custom_components/ha_behringer_mixer/switch.py b/custom_components/ha_behringer_mixer/switch.py new file mode 100644 index 0000000..2ac1e1f --- /dev/null +++ b/custom_components/ha_behringer_mixer/switch.py @@ -0,0 +1,61 @@ +"""Switch platform for behringer_mixer.""" +from __future__ import annotations + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription + +from .const import DOMAIN +from .entity import BehringerMixerEntity + + +async def async_setup_entry(hass, entry, async_add_devices): + """Set up the sensor platform.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + devices_list = build_entities(coordinator) + async_add_devices(devices_list) + + +def build_entities(coordinator): + """Build up the entities.""" + entities = [] + for entity in coordinator.entity_catalog.get("SWITCH"): + entities.append( + BehringerMixerSwitch( + coordinator=coordinator, + entity_description=SwitchEntityDescription( + key=entity.get("key"), + name=entity.get("default_name"), + ), + entity_setup=entity, + ) + ) + return entities + + +class BehringerMixerSwitch(BehringerMixerEntity, SwitchEntity): + """Behringer_mixer switch class.""" + + _attr_icon = "mdi:volume-high" + + @property + def icon(self) -> str | None: + """Icon of the entity.""" + if self.is_on: + return "mdi:volume-high" + return "mdi:volume-mute" + + @property + def is_on(self) -> bool: + """Return true if the switch is on.""" + return self.coordinator.data.get(self.base_address + "/mix_on", False) + + async def async_turn_on(self, **_: any) -> None: + """Turn on the switch.""" + await self.coordinator.client.async_set_value( + self.base_address + "/mix_on", True + ) + + async def async_turn_off(self, **_: any) -> None: + """Turn off the switch.""" + await self.coordinator.client.async_set_value( + self.base_address + "/mix_on", False + ) diff --git a/custom_components/integration_blueprint/translations/en.json b/custom_components/ha_behringer_mixer/translations/en.json similarity index 67% rename from custom_components/integration_blueprint/translations/en.json rename to custom_components/ha_behringer_mixer/translations/en.json index 049f7a4..5967d04 100644 --- a/custom_components/integration_blueprint/translations/en.json +++ b/custom_components/ha_behringer_mixer/translations/en.json @@ -2,10 +2,10 @@ "config": { "step": { "user": { - "description": "If you need help with the configuration have a look here: https://github.com/ludeeus/integration_blueprint", + "description": "If you need help with the configuration have a look here: https://github.com/wrodie/ha_behringer_mixer", "data": { - "username": "Username", - "password": "Password" + "MIXER_IP": "Mixer IP Address", + "MIXER_TYPE": "Mixer Type eg X32, XR12" } } }, @@ -15,4 +15,5 @@ "unknown": "Unknown error occurred." } } + } \ No newline at end of file diff --git a/custom_components/integration_blueprint/api.py b/custom_components/integration_blueprint/api.py deleted file mode 100644 index a738040..0000000 --- a/custom_components/integration_blueprint/api.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Sample API Client.""" -from __future__ import annotations - -import asyncio -import socket - -import aiohttp -import async_timeout - - -class IntegrationBlueprintApiClientError(Exception): - """Exception to indicate a general API error.""" - - -class IntegrationBlueprintApiClientCommunicationError( - IntegrationBlueprintApiClientError -): - """Exception to indicate a communication error.""" - - -class IntegrationBlueprintApiClientAuthenticationError( - IntegrationBlueprintApiClientError -): - """Exception to indicate an authentication error.""" - - -class IntegrationBlueprintApiClient: - """Sample API Client.""" - - def __init__( - self, - username: str, - password: str, - session: aiohttp.ClientSession, - ) -> None: - """Sample API Client.""" - self._username = username - self._password = password - self._session = session - - async def async_get_data(self) -> any: - """Get data from the API.""" - return await self._api_wrapper( - method="get", url="https://jsonplaceholder.typicode.com/posts/1" - ) - - async def async_set_title(self, value: str) -> any: - """Get data from the API.""" - return await self._api_wrapper( - method="patch", - url="https://jsonplaceholder.typicode.com/posts/1", - data={"title": value}, - headers={"Content-type": "application/json; charset=UTF-8"}, - ) - - async def _api_wrapper( - self, - method: str, - url: str, - data: dict | None = None, - headers: dict | None = None, - ) -> any: - """Get information from the API.""" - try: - async with async_timeout.timeout(10): - response = await self._session.request( - method=method, - url=url, - headers=headers, - json=data, - ) - if response.status in (401, 403): - raise IntegrationBlueprintApiClientAuthenticationError( - "Invalid credentials", - ) - response.raise_for_status() - return await response.json() - - except asyncio.TimeoutError as exception: - raise IntegrationBlueprintApiClientCommunicationError( - "Timeout error fetching information", - ) from exception - except (aiohttp.ClientError, socket.gaierror) as exception: - raise IntegrationBlueprintApiClientCommunicationError( - "Error fetching information", - ) from exception - except Exception as exception: # pylint: disable=broad-except - raise IntegrationBlueprintApiClientError( - "Something really wrong happened!" - ) from exception diff --git a/custom_components/integration_blueprint/binary_sensor.py b/custom_components/integration_blueprint/binary_sensor.py deleted file mode 100644 index fff5b21..0000000 --- a/custom_components/integration_blueprint/binary_sensor.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Binary sensor platform for integration_blueprint.""" -from __future__ import annotations - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntity, - BinarySensorEntityDescription, -) - -from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator -from .entity import IntegrationBlueprintEntity - -ENTITY_DESCRIPTIONS = ( - BinarySensorEntityDescription( - key="integration_blueprint", - name="Integration Blueprint Binary Sensor", - device_class=BinarySensorDeviceClass.CONNECTIVITY, - ), -) - - -async def async_setup_entry(hass, entry, async_add_devices): - """Set up the binary_sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices( - IntegrationBlueprintBinarySensor( - coordinator=coordinator, - entity_description=entity_description, - ) - for entity_description in ENTITY_DESCRIPTIONS - ) - - -class IntegrationBlueprintBinarySensor(IntegrationBlueprintEntity, BinarySensorEntity): - """integration_blueprint binary_sensor class.""" - - def __init__( - self, - coordinator: BlueprintDataUpdateCoordinator, - entity_description: BinarySensorEntityDescription, - ) -> None: - """Initialize the binary_sensor class.""" - super().__init__(coordinator) - self.entity_description = entity_description - - @property - def is_on(self) -> bool: - """Return true if the binary_sensor is on.""" - return self.coordinator.data.get("title", "") == "foo" diff --git a/custom_components/integration_blueprint/config_flow.py b/custom_components/integration_blueprint/config_flow.py deleted file mode 100644 index a474163..0000000 --- a/custom_components/integration_blueprint/config_flow.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Adds config flow for Blueprint.""" -from __future__ import annotations - -import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import selector -from homeassistant.helpers.aiohttp_client import async_create_clientsession - -from .api import ( - IntegrationBlueprintApiClient, - IntegrationBlueprintApiClientAuthenticationError, - IntegrationBlueprintApiClientCommunicationError, - IntegrationBlueprintApiClientError, -) -from .const import DOMAIN, LOGGER - - -class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Config flow for Blueprint.""" - - VERSION = 1 - - async def async_step_user( - self, - user_input: dict | None = None, - ) -> config_entries.FlowResult: - """Handle a flow initialized by the user.""" - _errors = {} - if user_input is not None: - try: - await self._test_credentials( - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - ) - except IntegrationBlueprintApiClientAuthenticationError as exception: - LOGGER.warning(exception) - _errors["base"] = "auth" - except IntegrationBlueprintApiClientCommunicationError as exception: - LOGGER.error(exception) - _errors["base"] = "connection" - except IntegrationBlueprintApiClientError as exception: - LOGGER.exception(exception) - _errors["base"] = "unknown" - else: - return self.async_create_entry( - title=user_input[CONF_USERNAME], - data=user_input, - ) - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required( - CONF_USERNAME, - default=(user_input or {}).get(CONF_USERNAME), - ): selector.TextSelector( - selector.TextSelectorConfig( - type=selector.TextSelectorType.TEXT - ), - ), - vol.Required(CONF_PASSWORD): selector.TextSelector( - selector.TextSelectorConfig( - type=selector.TextSelectorType.PASSWORD - ), - ), - } - ), - errors=_errors, - ) - - async def _test_credentials(self, username: str, password: str) -> None: - """Validate credentials.""" - client = IntegrationBlueprintApiClient( - username=username, - password=password, - session=async_create_clientsession(self.hass), - ) - await client.async_get_data() diff --git a/custom_components/integration_blueprint/coordinator.py b/custom_components/integration_blueprint/coordinator.py deleted file mode 100644 index d427a1a..0000000 --- a/custom_components/integration_blueprint/coordinator.py +++ /dev/null @@ -1,49 +0,0 @@ -"""DataUpdateCoordinator for integration_blueprint.""" -from __future__ import annotations - -from datetime import timedelta - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import ( - DataUpdateCoordinator, - UpdateFailed, -) -from homeassistant.exceptions import ConfigEntryAuthFailed - -from .api import ( - IntegrationBlueprintApiClient, - IntegrationBlueprintApiClientAuthenticationError, - IntegrationBlueprintApiClientError, -) -from .const import DOMAIN, LOGGER - - -# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities -class BlueprintDataUpdateCoordinator(DataUpdateCoordinator): - """Class to manage fetching data from the API.""" - - config_entry: ConfigEntry - - def __init__( - self, - hass: HomeAssistant, - client: IntegrationBlueprintApiClient, - ) -> None: - """Initialize.""" - self.client = client - super().__init__( - hass=hass, - logger=LOGGER, - name=DOMAIN, - update_interval=timedelta(minutes=5), - ) - - async def _async_update_data(self): - """Update data via library.""" - try: - return await self.client.async_get_data() - except IntegrationBlueprintApiClientAuthenticationError as exception: - raise ConfigEntryAuthFailed(exception) from exception - except IntegrationBlueprintApiClientError as exception: - raise UpdateFailed(exception) from exception diff --git a/custom_components/integration_blueprint/entity.py b/custom_components/integration_blueprint/entity.py deleted file mode 100644 index 4325227..0000000 --- a/custom_components/integration_blueprint/entity.py +++ /dev/null @@ -1,25 +0,0 @@ -"""BlueprintEntity class.""" -from __future__ import annotations - -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import ATTRIBUTION, DOMAIN, NAME, VERSION -from .coordinator import BlueprintDataUpdateCoordinator - - -class IntegrationBlueprintEntity(CoordinatorEntity): - """BlueprintEntity class.""" - - _attr_attribution = ATTRIBUTION - - def __init__(self, coordinator: BlueprintDataUpdateCoordinator) -> None: - """Initialize.""" - super().__init__(coordinator) - self._attr_unique_id = coordinator.config_entry.entry_id - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, - name=NAME, - model=VERSION, - manufacturer=NAME, - ) diff --git a/custom_components/integration_blueprint/manifest.json b/custom_components/integration_blueprint/manifest.json deleted file mode 100644 index 817cd7b..0000000 --- a/custom_components/integration_blueprint/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "integration_blueprint", - "name": "Integration blueprint", - "codeowners": [ - "@ludeeus" - ], - "config_flow": true, - "documentation": "https://github.com/ludeeus/integration_blueprint", - "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/ludeeus/integration_blueprint/issues", - "version": "0.0.0" -} \ No newline at end of file diff --git a/custom_components/integration_blueprint/sensor.py b/custom_components/integration_blueprint/sensor.py deleted file mode 100644 index 06201fe..0000000 --- a/custom_components/integration_blueprint/sensor.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Sensor platform for integration_blueprint.""" -from __future__ import annotations - -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription - -from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator -from .entity import IntegrationBlueprintEntity - -ENTITY_DESCRIPTIONS = ( - SensorEntityDescription( - key="integration_blueprint", - name="Integration Sensor", - icon="mdi:format-quote-close", - ), -) - - -async def async_setup_entry(hass, entry, async_add_devices): - """Set up the sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices( - IntegrationBlueprintSensor( - coordinator=coordinator, - entity_description=entity_description, - ) - for entity_description in ENTITY_DESCRIPTIONS - ) - - -class IntegrationBlueprintSensor(IntegrationBlueprintEntity, SensorEntity): - """integration_blueprint Sensor class.""" - - def __init__( - self, - coordinator: BlueprintDataUpdateCoordinator, - entity_description: SensorEntityDescription, - ) -> None: - """Initialize the sensor class.""" - super().__init__(coordinator) - self.entity_description = entity_description - - @property - def native_value(self) -> str: - """Return the native value of the sensor.""" - return self.coordinator.data.get("body") diff --git a/custom_components/integration_blueprint/switch.py b/custom_components/integration_blueprint/switch.py deleted file mode 100644 index 33340a2..0000000 --- a/custom_components/integration_blueprint/switch.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Switch platform for integration_blueprint.""" -from __future__ import annotations - -from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription - -from .const import DOMAIN -from .coordinator import BlueprintDataUpdateCoordinator -from .entity import IntegrationBlueprintEntity - -ENTITY_DESCRIPTIONS = ( - SwitchEntityDescription( - key="integration_blueprint", - name="Integration Switch", - icon="mdi:format-quote-close", - ), -) - - -async def async_setup_entry(hass, entry, async_add_devices): - """Set up the sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices( - IntegrationBlueprintSwitch( - coordinator=coordinator, - entity_description=entity_description, - ) - for entity_description in ENTITY_DESCRIPTIONS - ) - - -class IntegrationBlueprintSwitch(IntegrationBlueprintEntity, SwitchEntity): - """integration_blueprint switch class.""" - - def __init__( - self, - coordinator: BlueprintDataUpdateCoordinator, - entity_description: SwitchEntityDescription, - ) -> None: - """Initialize the switch class.""" - super().__init__(coordinator) - self.entity_description = entity_description - - @property - def is_on(self) -> bool: - """Return true if the switch is on.""" - return self.coordinator.data.get("title", "") == "foo" - - async def async_turn_on(self, **_: any) -> None: - """Turn on the switch.""" - await self.coordinator.api.async_set_title("bar") - await self.coordinator.async_request_refresh() - - async def async_turn_off(self, **_: any) -> None: - """Turn off the switch.""" - await self.coordinator.api.async_set_title("foo") - await self.coordinator.async_request_refresh() diff --git a/hacs.json b/hacs.json index 65f7335..7509e95 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { - "name": "Integration blueprint", - "filename": "integration_blueprint.zip", + "name": "Behringer Mixer", + "filename": "ha_behringer_mixer.zip", "hide_default_branch": true, "homeassistant": "2023.3.0", "render_readme": true, diff --git a/requirements.txt b/requirements.txt index b44f981..b605856 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ colorlog==6.7.0 homeassistant==2023.2.0 pip>=21.0,<23.4 -ruff==0.0.267 +ruff==0.1.3 diff --git a/scripts/develop b/scripts/develop index 89eda50..0b2a39d 100755 --- a/scripts/develop +++ b/scripts/develop @@ -11,7 +11,7 @@ if [[ ! -d "${PWD}/config" ]]; then fi # Set the path to custom_components -## This let's us have the structure we want /custom_components/integration_blueprint +## This let's us have the structure we want /custom_components/ha_behringer_mixer ## while at the same time have Home Assistant configuration inside /config ## without resulting to symlinks. export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"