-
-
Notifications
You must be signed in to change notification settings - Fork 38.3k
Config Flow and Entity registry support for Monoprice #30337
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3d0ec94
0ac418b
7d26784
4159563
c2e64fe
b96cf26
12aad06
0bd57e0
552d117
fc3243e
6a855da
0c4689c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,36 @@ | ||
| """The monoprice component.""" | ||
| """The Monoprice 6-Zone Amplifier integration.""" | ||
| import asyncio | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.core import HomeAssistant | ||
|
|
||
| PLATFORMS = ["media_player"] | ||
|
|
||
|
|
||
| async def async_setup(hass: HomeAssistant, config: dict): | ||
| """Set up the Monoprice 6-Zone Amplifier component.""" | ||
| return True | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): | ||
| """Set up Monoprice 6-Zone Amplifier from a config entry.""" | ||
| for component in PLATFORMS: | ||
| hass.async_create_task( | ||
| hass.config_entries.async_forward_entry_setup(entry, component) | ||
| ) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): | ||
| """Unload a config entry.""" | ||
| unload_ok = all( | ||
| await asyncio.gather( | ||
| *[ | ||
| hass.config_entries.async_forward_entry_unload(entry, component) | ||
| for component in PLATFORMS | ||
| ] | ||
| ) | ||
| ) | ||
|
|
||
| return unload_ok |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| """Config flow for Monoprice 6-Zone Amplifier integration.""" | ||
| import logging | ||
|
|
||
| from pymonoprice import get_async_monoprice | ||
| from serial import SerialException | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant import config_entries, core, exceptions | ||
| from homeassistant.const import CONF_PORT | ||
|
|
||
| from .const import ( | ||
| CONF_SOURCE_1, | ||
| CONF_SOURCE_2, | ||
| CONF_SOURCE_3, | ||
| CONF_SOURCE_4, | ||
| CONF_SOURCE_5, | ||
| CONF_SOURCE_6, | ||
| CONF_SOURCES, | ||
| ) | ||
| from .const import DOMAIN # pylint:disable=unused-import | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| DATA_SCHEMA = vol.Schema( | ||
| { | ||
| vol.Required(CONF_PORT): str, | ||
| vol.Optional(CONF_SOURCE_1): str, | ||
| vol.Optional(CONF_SOURCE_2): str, | ||
| vol.Optional(CONF_SOURCE_3): str, | ||
| vol.Optional(CONF_SOURCE_4): str, | ||
| vol.Optional(CONF_SOURCE_5): str, | ||
| vol.Optional(CONF_SOURCE_6): str, | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| async def validate_input(hass: core.HomeAssistant, data): | ||
| """Validate the user input allows us to connect. | ||
|
|
||
| Data has the keys from DATA_SCHEMA with values provided by the user. | ||
| """ | ||
| try: | ||
| await get_async_monoprice(data[CONF_PORT], hass.loop) | ||
| except SerialException: | ||
| _LOGGER.error("Error connecting to Monoprice controller") | ||
| raise CannotConnect | ||
|
|
||
| sources_config = { | ||
| 1: data.get(CONF_SOURCE_1), | ||
| 2: data.get(CONF_SOURCE_2), | ||
| 3: data.get(CONF_SOURCE_3), | ||
| 4: data.get(CONF_SOURCE_4), | ||
| 5: data.get(CONF_SOURCE_5), | ||
| 6: data.get(CONF_SOURCE_6), | ||
| } | ||
| sources = { | ||
| index: name.strip() | ||
| for index, name in sources_config.items() | ||
| if (name is not None and name.strip() != "") | ||
| } | ||
| # Return info that you want to store in the config entry. | ||
| return {CONF_PORT: data[CONF_PORT], CONF_SOURCES: sources} | ||
|
|
||
|
|
||
| class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
| """Handle a config flow for Monoprice 6-Zone Amplifier.""" | ||
|
|
||
| VERSION = 1 | ||
| CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL | ||
|
|
||
| async def async_step_user(self, user_input=None): | ||
| """Handle the initial step.""" | ||
| errors = {} | ||
| if user_input is not None: | ||
| try: | ||
| info = await validate_input(self.hass, user_input) | ||
|
|
||
| return self.async_create_entry(title=user_input[CONF_PORT], data=info) | ||
| except CannotConnect: | ||
| errors["base"] = "cannot_connect" | ||
| except Exception: # pylint: disable=broad-except | ||
| _LOGGER.exception("Unexpected exception") | ||
| errors["base"] = "unknown" | ||
|
|
||
| return self.async_show_form( | ||
| step_id="user", data_schema=DATA_SCHEMA, errors=errors | ||
| ) | ||
|
|
||
|
|
||
| class CannotConnect(exceptions.HomeAssistantError): | ||
| """Error to indicate we cannot connect.""" | ||
|
|
||
|
|
||
| class InvalidAuth(exceptions.HomeAssistantError): | ||
| """Error to indicate there is invalid auth.""" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,15 @@ | ||
| """Constants for the Monoprice 6-Zone Amplifier Media Player component.""" | ||
|
|
||
| DOMAIN = "monoprice" | ||
|
|
||
| CONF_SOURCES = "sources" | ||
|
|
||
| CONF_SOURCE_1 = "source_1" | ||
| CONF_SOURCE_2 = "source_2" | ||
| CONF_SOURCE_3 = "source_3" | ||
| CONF_SOURCE_4 = "source_4" | ||
| CONF_SOURCE_5 = "source_5" | ||
| CONF_SOURCE_6 = "source_6" | ||
|
|
||
| SERVICE_SNAPSHOT = "snapshot" | ||
| SERVICE_RESTORE = "restore" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,9 +3,8 @@ | |
|
|
||
| from pymonoprice import get_monoprice | ||
| from serial import SerialException | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice | ||
| from homeassistant.components.media_player import MediaPlayerDevice | ||
| from homeassistant.components.media_player.const import ( | ||
| SUPPORT_SELECT_SOURCE, | ||
| SUPPORT_TURN_OFF, | ||
|
|
@@ -14,16 +13,10 @@ | |
| SUPPORT_VOLUME_SET, | ||
| SUPPORT_VOLUME_STEP, | ||
| ) | ||
| from homeassistant.const import ( | ||
| ATTR_ENTITY_ID, | ||
| CONF_NAME, | ||
| CONF_PORT, | ||
| STATE_OFF, | ||
| STATE_ON, | ||
| ) | ||
| import homeassistant.helpers.config_validation as cv | ||
| from homeassistant.const import CONF_PORT, STATE_OFF, STATE_ON | ||
| from homeassistant.helpers import config_validation as cv, entity_platform, service | ||
|
|
||
| from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT | ||
| from .const import CONF_SOURCES, DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -36,104 +29,89 @@ | |
| | SUPPORT_SELECT_SOURCE | ||
| ) | ||
|
|
||
| ZONE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) | ||
|
|
||
| SOURCE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) | ||
|
|
||
| CONF_ZONES = "zones" | ||
| CONF_SOURCES = "sources" | ||
| def _get_sources(sources_config): | ||
| source_id_name = {int(index): name for index, name in sources_config.items()} | ||
|
|
||
| DATA_MONOPRICE = "monoprice" | ||
| source_name_id = {v: k for k, v in source_id_name.items()} | ||
|
|
||
| # Valid zone ids: 11-16 or 21-26 or 31-36 | ||
| ZONE_IDS = vol.All( | ||
| vol.Coerce(int), | ||
| vol.Any( | ||
| vol.Range(min=11, max=16), vol.Range(min=21, max=26), vol.Range(min=31, max=36) | ||
| ), | ||
| ) | ||
| source_names = sorted(source_name_id.keys(), key=lambda v: source_name_id[v]) | ||
|
|
||
| # Valid source ids: 1-6 | ||
| SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=6)) | ||
| return [source_id_name, source_name_id, source_names] | ||
|
|
||
| MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) | ||
|
|
||
| PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( | ||
| { | ||
| vol.Required(CONF_PORT): cv.string, | ||
| vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), | ||
| vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def setup_platform(hass, config, add_entities, discovery_info=None): | ||
| async def async_setup_entry(hass, config_entry, async_add_devices): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please rename |
||
| """Set up the Monoprice 6-zone amplifier platform.""" | ||
| port = config.get(CONF_PORT) | ||
| port = config_entry.data.get(CONF_PORT) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Port is required so we can use |
||
|
|
||
| try: | ||
| monoprice = get_monoprice(port) | ||
| monoprice = await hass.async_add_executor_job(get_monoprice, port) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we do this in
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @MartinHjelmare that's my goal, and the code changes are relatively straightforward. The reason I'm not doing this yet is that |
||
| except SerialException: | ||
| _LOGGER.error("Error connecting to Monoprice controller") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You want to add the port in case you have multiple set up. |
||
| return | ||
|
|
||
| sources = { | ||
| source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items() | ||
| } | ||
| sources = _get_sources(config_entry.data.get(CONF_SOURCES)) | ||
|
|
||
| devices = [] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please rename |
||
| for i in range(1, 4): | ||
| for j in range(1, 7): | ||
| zone_id = (i * 10) + j | ||
| _LOGGER.info("Adding zone %d for port %s", zone_id, port) | ||
| devices.append( | ||
| MonopriceZone(monoprice, sources, config_entry.entry_id, zone_id) | ||
| ) | ||
|
|
||
| hass.data[DATA_MONOPRICE] = [] | ||
| for zone_id, extra in config[CONF_ZONES].items(): | ||
| _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) | ||
| hass.data[DATA_MONOPRICE].append( | ||
| MonopriceZone(monoprice, sources, zone_id, extra[CONF_NAME]) | ||
| ) | ||
| async_add_devices(devices, True) | ||
|
|
||
| add_entities(hass.data[DATA_MONOPRICE], True) | ||
| platform = entity_platform.current_platform.get() | ||
|
|
||
| def service_handle(service): | ||
| def _call_service(entities, service_call): | ||
| for entity in entities: | ||
| if service_call.service == SERVICE_SNAPSHOT: | ||
| entity.snapshot() | ||
| elif service_call.service == SERVICE_RESTORE: | ||
| entity.restore() | ||
|
|
||
| @service.verify_domain_control(hass, DOMAIN) | ||
| async def async_service_handle(service_call): | ||
| """Handle for services.""" | ||
| entity_ids = service.data.get(ATTR_ENTITY_ID) | ||
|
|
||
| if entity_ids: | ||
| devices = [ | ||
| device | ||
| for device in hass.data[DATA_MONOPRICE] | ||
| if device.entity_id in entity_ids | ||
| ] | ||
| else: | ||
| devices = hass.data[DATA_MONOPRICE] | ||
| entities = await platform.async_extract_from_service(service_call) | ||
|
|
||
| for device in devices: | ||
| if service.service == SERVICE_SNAPSHOT: | ||
| device.snapshot() | ||
| elif service.service == SERVICE_RESTORE: | ||
| device.restore() | ||
| if not entities: | ||
| return | ||
|
|
||
| hass.async_add_executor_job(_call_service, entities, service_call) | ||
|
|
||
| hass.services.register( | ||
| DOMAIN, SERVICE_SNAPSHOT, service_handle, schema=MEDIA_PLAYER_SCHEMA | ||
| hass.services.async_register( | ||
| DOMAIN, | ||
| SERVICE_SNAPSHOT, | ||
| async_service_handle, | ||
| schema=cv.make_entity_service_schema({}), | ||
| ) | ||
|
|
||
| hass.services.register( | ||
| DOMAIN, SERVICE_RESTORE, service_handle, schema=MEDIA_PLAYER_SCHEMA | ||
| hass.services.async_register( | ||
| DOMAIN, | ||
| SERVICE_RESTORE, | ||
| async_service_handle, | ||
| schema=cv.make_entity_service_schema({}), | ||
| ) | ||
|
|
||
|
|
||
| class MonopriceZone(MediaPlayerDevice): | ||
| """Representation of a Monoprice amplifier zone.""" | ||
|
|
||
| def __init__(self, monoprice, sources, zone_id, zone_name): | ||
| def __init__(self, monoprice, sources, namespace, zone_id): | ||
| """Initialize new zone.""" | ||
| self._monoprice = monoprice | ||
| # dict source_id -> source name | ||
| self._source_id_name = sources | ||
| self._source_id_name = sources[0] | ||
| # dict source name -> source_id | ||
| self._source_name_id = {v: k for k, v in sources.items()} | ||
| self._source_name_id = sources[1] | ||
| # ordered list of all source names | ||
| self._source_names = sorted( | ||
| self._source_name_id.keys(), key=lambda v: self._source_name_id[v] | ||
| ) | ||
| self._source_names = sources[2] | ||
| self._zone_id = zone_id | ||
| self._name = zone_name | ||
| self._unique_id = f"{namespace}_{self._zone_id}" | ||
| self._name = f"Zone {self._zone_id}" | ||
|
|
||
| self._snapshot = None | ||
| self._state = None | ||
|
|
@@ -156,6 +134,26 @@ def update(self): | |
| self._source = None | ||
| return True | ||
|
|
||
| @property | ||
| def entity_registry_enabled_default(self): | ||
| """Return if the entity should be enabled when first added to the entity registry.""" | ||
| return self._zone_id < 20 | ||
|
|
||
| @property | ||
| def device_info(self): | ||
| """Return device info for this device.""" | ||
| return { | ||
| "identifiers": {(DOMAIN, self.unique_id)}, | ||
| "name": self.name, | ||
| "manufacturer": "Monoprice", | ||
| "model": "6-Zone Amplifier", | ||
| } | ||
|
|
||
| @property | ||
| def unique_id(self): | ||
| """Return unique ID for this device.""" | ||
| return self._unique_id | ||
|
|
||
| @property | ||
| def name(self): | ||
| """Return the name of the zone.""" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| { | ||
| "config": { | ||
| "title": "Monoprice 6-Zone Amplifier", | ||
| "step": { | ||
| "user": { | ||
| "title": "Connect to the device", | ||
| "data": { | ||
| "port": "Serial port", | ||
| "source_1": "Name of source #1", | ||
| "source_2": "Name of source #2", | ||
| "source_3": "Name of source #3", | ||
| "source_4": "Name of source #4", | ||
| "source_5": "Name of source #5", | ||
| "source_6": "Name of source #6" | ||
| } | ||
| } | ||
| }, | ||
| "error": { | ||
| "cannot_connect": "Failed to connect, please try again", | ||
| "unknown": "Unexpected error" | ||
| }, | ||
| "abort": { | ||
| "already_configured": "Device is already configured" | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't used.