Skip to content

Commit

Permalink
added config_flow and test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
dreed47 committed May 15, 2021
1 parent 7c6c395 commit 5f17e4b
Show file tree
Hide file tree
Showing 16 changed files with 434 additions and 60 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
venv
.venv
.vscode
*.pyc
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is a _Custom Integration_ for [Home Assistant](https://www.home-assistant.io/). It uses the unofficial [Redfin](https://www.redfin.com) API to get property value estimates.

![GitHub release](https://img.shields.io/badge/release-v1.0.2-blue)
![GitHub release](https://img.shields.io/badge/release-v1.1.0-blue)
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs)

There is currently support for the Sensor device type within Home Assistant.
Expand All @@ -22,7 +22,11 @@ To manually add Redfin to your installation, create this folder structure in you
Then drop the following files into that folder:

```yaml
translations/en.json
__init__.py
config_flow.py
const.py
hacs.json
sensor.py
manifest.json
```
Expand All @@ -33,15 +37,7 @@ You will need the Redfin property ID for each property you’d like to track. Th

For example, given this Redfin URL: https://www.redfin.com/DC/Washington/1745-Q-St-NW-20009/unit-3/home/9860590 the property ID is 9860590.

To enable this sensor, add the following lines to your `configuration.yaml`.

```yaml
sensor:
- platform: redfin
property_ids:
- "12345678"
- "34567890"
```
To enable this sensor, add new Redfin integration component in the Home Assistant UI and follow the prompts to add your properties.

The sensor provides the following attributes:

Expand Down
1 change: 1 addition & 0 deletions custom_components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
""""""
53 changes: 52 additions & 1 deletion custom_components/redfin/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
"""The Redfin component."""
"""Redfin Component."""
import logging

from homeassistant import config_entries, core

from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType

from .const import DOMAIN, CONF_PROPERTIES
from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# We allow setup only through config flow type of config
return True


async def async_setup_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Set up Redfin component from a ConfigEntry."""
hass.data.setdefault(DOMAIN, {})
hass_data = dict()
hass_data[CONF_NAME] = entry.options[CONF_NAME]
hass_data[CONF_SCAN_INTERVAL] = entry.options[CONF_SCAN_INTERVAL]
hass_data[CONF_PROPERTIES] = entry.options[CONF_PROPERTIES]
# Registers update listener to update config entry when options are updated.
unsub_options_update_listener = entry.add_update_listener(options_update_listener)
# Store a reference to the unsubscribe function to cleanup if an entry is unloaded.
hass_data["unsub_options_update_listener"] = unsub_options_update_listener
hass.data[DOMAIN][entry.entry_id] = hass_data

# Forward the setup to the sensor component.
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
return True


async def options_update_listener(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
):
_LOGGER.debug("%s options updated: %s", DOMAIN, config_entry.as_dict()["options"])
"""Handle options update."""
try:
result = await hass.config_entries.async_reload(config_entry.entry_id)
except config_entries.OperationNotAllowed:
_LOGGER.error("Entry cannot be reloaded. ID = %s Restart is required.", config_entry.entry_id)
except config_entries.UnknownEntry:
_LOGGER.error("Invalid entry specified. ID = %s", config_entry.entry_id)
179 changes: 179 additions & 0 deletions custom_components/redfin/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
from copy import deepcopy
import logging
from typing import Any, Dict, Optional

from homeassistant import config_entries, core
from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.helpers.entity_registry import (
async_entries_for_config_entry,
async_get_registry,
)

from .const import (CONF_PROPERTIES, DOMAIN, DEFAULT_SCAN_INTERVAL, DEFAULT_NAME,
CONF_PROPERTY_ID, CONF_SCAN_INTERVAL_MIN, CONF_SCAN_INTERVAL_MAX)

_LOGGER = logging.getLogger(__name__)

CONF_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): vol.All(vol.Coerce(int), vol.Range(min=CONF_SCAN_INTERVAL_MIN, max=CONF_SCAN_INTERVAL_MAX))
}
)
PROPERTY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_PROPERTY_ID): cv.string,
vol.Optional("add_another"): cv.boolean,
}
)


class RedfinConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Redfin config flow."""

VERSION = 1

data: Optional[Dict[str, Any]]
options: Optional[Dict[str, Any]] = {}

async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None):
"""Invoked when a user initiates a flow via the user interface."""
errors: Dict[str, str] = {}

if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if self.hass.data.get(DOMAIN):
return self.async_abort(reason="single_instance_allowed")

if user_input is not None:
self.data = dict() # user_input
self.options = user_input
self.options[CONF_PROPERTIES] = []
# Return the form of the next step.
return await self.async_step_property()

return self.async_show_form(
step_id="user", data_schema=CONF_SCHEMA, errors=errors
)

async def async_step_property(self, user_input: Optional[Dict[str, Any]] = None):
"""Second step in config flow to add a property id's."""
errors: Dict[str, str] = {}

if user_input is not None:

# check for duplicate property id's
is_dup = False
for params in self.options[CONF_PROPERTIES]:
if user_input[CONF_PROPERTY_ID] == params[CONF_PROPERTY_ID]:
is_dup = True
if is_dup == True:
errors["base"] = "duplicate_property_id"
else:
self.options[CONF_PROPERTIES].append(
{"property_id": user_input[CONF_PROPERTY_ID]}
)

if not errors:
# If user ticked the box show this form again so they can add
if user_input.get("add_another", False):
return await self.async_step_property()

# User is done adding properties, create the config entry.
_LOGGER.debug("%s component added a new config entry: %s", DOMAIN, self.options)
return self.async_create_entry(title=self.options["name"], data=self.data, options=self.options)

return self.async_show_form(
step_id="property", data_schema=PROPERTY_SCHEMA, errors=errors
)

@staticmethod
@callback
def async_get_options_flow(config_entry):
return OptionsFlowHandler(config_entry)


class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handles options flow for the component."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
self.config_entry = config_entry

async def async_step_init(
self, user_input: Dict[str, Any] = None
) -> Dict[str, Any]:
"""Manage the options for the component."""
errors: Dict[str, str] = {}
# Grab all configured propert id's from the entity registry so we can populate the
# multi-select dropdown that will allow a user to remove a property.
entity_registry = await async_get_registry(self.hass)
entries = async_entries_for_config_entry(
entity_registry, self.config_entry.entry_id
)
# Default value for our multi-select.
all_properties = {e.entity_id: e.original_name for e in entries}
property_map = {e.entity_id: e for e in entries}

if user_input is not None:
updated_properties = deepcopy(self.config_entry.options[CONF_PROPERTIES])

# Remove any unchecked properties.
removed_entities = [
entity_id
for entity_id in property_map.keys()
if entity_id not in user_input["properties"]
]
for entity_id in removed_entities:
# Unregister from HA
entity_registry.async_remove(entity_id)
# Remove from our configured properties.
entry = property_map[entity_id]
property_id = entry.unique_id
updated_properties = [e for e in updated_properties if e[CONF_PROPERTY_ID] != property_id]

if user_input.get(CONF_PROPERTY_ID):

# check for duplicate property id's
is_dup = False
for params in updated_properties:
if user_input[CONF_PROPERTY_ID] == params[CONF_PROPERTY_ID]:
is_dup = True
if is_dup == True:
errors["base"] = "duplicate_property_id"
else:
# Add the new property.
updated_properties.append(
{CONF_PROPERTY_ID: user_input[CONF_PROPERTY_ID]}
)

if not errors:
# Value of data will be set on the options property of the config_entry
# instance.
return self.async_create_entry(
title="",
data={
CONF_NAME: user_input[CONF_NAME],
CONF_SCAN_INTERVAL: user_input[CONF_SCAN_INTERVAL],
CONF_PROPERTIES: updated_properties
},
)

options_schema = vol.Schema(
{
vol.Optional("properties", default=list(all_properties.keys())): cv.multi_select(
all_properties
),
vol.Optional(CONF_NAME, default=self.config_entry.options[CONF_NAME]): str,
vol.Optional(CONF_SCAN_INTERVAL, default=self.config_entry.options[CONF_SCAN_INTERVAL]
): vol.All(vol.Coerce(int), vol.Range(min=CONF_SCAN_INTERVAL_MIN, max=CONF_SCAN_INTERVAL_MAX)),
vol.Optional(CONF_PROPERTY_ID): cv.string,
}
)

return self.async_show_form(
step_id="init", data_schema=options_schema, errors=errors
)
21 changes: 21 additions & 0 deletions custom_components/redfin/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
DOMAIN = "redfin"

DEFAULT_NAME = "Redfin"

ATTRIBUTION = "Data provided by Redfin.com"
ATTR_AMOUNT = "amount"
ATTR_AMOUNT_FORMATTED = "amount_formatted"
ATTR_ADDRESS = "address"
ATTR_FULL_ADDRESS = "full_address"
ATTR_CURRENCY = "amount_currency"
ATTR_STREET_VIEW = "street_view"
ATTR_REDFIN_URL = "redfin_url"

CONF_PROPERTIES = "properties"
CONF_PROPERTY_ID = "property_id"
CONF_PROPERTY_IDS = "property_ids"
DEFAULT_SCAN_INTERVAL = 60
CONF_SCAN_INTERVAL_MIN = 5
CONF_SCAN_INTERVAL_MAX = 600

ICON = "mdi:home-variant"
3 changes: 2 additions & 1 deletion custom_components/redfin/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"codeowners": ["@dreed47"],
"requirements": ["redfin==0.1.1"],
"iot_class": "cloud_polling",
"version": "1.0.2"
"version": "1.1.0",
"config_flow": true
}
Loading

0 comments on commit 5f17e4b

Please sign in to comment.