diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a3069..79daa23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2020-04-19 + +**Fixed bugs:** + +- Fix issue [\#51](https://github.com/elad-bar/ha-bleuiris/issues/51) in config_flow +- Validation of server existence made 2 calls to server instead of 1 + ## 2020-04-18 **Implemented enhancements:** diff --git a/custom_components/blueiris/api/blue_iris_api.py b/custom_components/blueiris/api/blue_iris_api.py index 64d1a14..67f7974 100644 --- a/custom_components/blueiris/api/blue_iris_api.py +++ b/custom_components/blueiris/api/blue_iris_api.py @@ -2,6 +2,8 @@ import json import hashlib import logging +from typing import Optional + import aiohttp from datetime import datetime @@ -21,7 +23,7 @@ class BlueIrisApi: """The Class for handling the data retrieval.""" is_logged_in: bool - session_id: str + session_id: Optional[str] session: ClientSession data: dict status: dict @@ -36,6 +38,7 @@ def __init__(self, hass: HomeAssistant, config_manager: ConfigManager): self._last_update = datetime.now() self.hass = hass self.config_manager = config_manager + self.session_id = None except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() @@ -134,7 +137,11 @@ async def load_session_id(self): response = await self.async_post(request_data) - self.session_id = response.get("session") + self.session_id = None + + if response is not None: + self.session_id = response.get("session") + self.is_logged_in = False async def login(self): @@ -145,33 +152,34 @@ async def login(self): try: await self.load_session_id() - config_data = self.config_manager.data - username = config_data.username - password = config_data.password_clear_text + if self.session_id is not None: + config_data = self.config_manager.data + username = config_data.username + password = config_data.password_clear_text - token_request = f"{username}:{self.session_id}:{password}" - m = hashlib.md5() - m.update(token_request.encode('utf-8')) - token = m.hexdigest() + token_request = f"{username}:{self.session_id}:{password}" + m = hashlib.md5() + m.update(token_request.encode('utf-8')) + token = m.hexdigest() - request_data = { - "cmd": "login", - "session": self.session_id, - "response": token - } + request_data = { + "cmd": "login", + "session": self.session_id, + "response": token + } - result = await self.async_post(request_data) + result = await self.async_post(request_data) - if result is not None: - result_status = result.get("result") + if result is not None: + result_status = result.get("result") - if result_status == "success": - logged_in = True + if result_status == "success": + logged_in = True - data = result.get("data", {}) + data = result.get("data", {}) - for key in data: - self.data[key] = data[key] + for key in data: + self.data[key] = data[key] except Exception as ex: exc_type, exc_obj, tb = sys.exc_info() diff --git a/custom_components/blueiris/config_flow.py b/custom_components/blueiris/config_flow.py index 9a926ed..f44b328 100644 --- a/custom_components/blueiris/config_flow.py +++ b/custom_components/blueiris/config_flow.py @@ -3,70 +3,27 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import callback from . import get_ha -from .managers.configuration_manager import ConfigManager -from .managers.password_manager import PasswordManager -from .api.blue_iris_api import BlueIrisApi +from .managers.config_flow_manager import ConfigFlowManager from .helpers.const import * -from .models.config_data import ConfigData _LOGGER = logging.getLogger(__name__) -class BlueIrisConfigFlow: - config_manager: ConfigManager - password_manager: PasswordManager - is_initialized: bool = False - - def initialize(self, hass: HomeAssistant): - if not self.is_initialized: - self.password_manager = PasswordManager(hass) - self.config_manager = ConfigManager(self.password_manager) - - self.is_initialized = True - - def update_config_data(self, data: dict, options: dict = None): - entry = ConfigEntry(0, "", "", data, "", "", {}, options=options) - - self.config_manager.update(entry) - - async def valid_login(self, hass): - errors = None - - config_data = self.config_manager.data - - api = BlueIrisApi(hass, self.config_manager) - await api.initialize() - - if not api.is_logged_in: - _LOGGER.warning(f"Failed to access BlueIris Server ({config_data.host})") - errors = { - "base": "invalid_server_details" - } - else: - has_credentials = config_data.has_credentials - - if has_credentials and not api.data.get("admin", False): - _LOGGER.warning(f"Failed to login BlueIris ({config_data.host}) due to invalid credentials") - errors = { - "base": "invalid_admin_credentials" - } - - return { - "logged-in": errors is None, - "errors": errors - } - - @config_entries.HANDLERS.register(DOMAIN) -class BlueIrisFlowHandler(config_entries.ConfigFlow, BlueIrisConfigFlow): +class BlueIrisFlowHandler(config_entries.ConfigFlow): """Handle a BlueIris config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + def __init__(self): + super().__init__() + + self._config_flow = ConfigFlowManager() + @staticmethod @callback def async_get_options_flow(config_entry): @@ -84,21 +41,17 @@ async def async_step_user(self, user_input=None): errors = None - self.initialize(self.hass) + self._config_flow.initialize(self.hass) if user_input is not None: - if CONF_PASSWORD in user_input: - password = user_input[CONF_PASSWORD] - user_input[CONF_PASSWORD] = self.password_manager.encrypt(password) - - self.update_config_data(user_input) + self._config_flow.update_data(user_input, True) host = self._config_flow.config_data.host ha = get_ha(self.hass, host) if ha is None: - result = await self.valid_login(self.hass) + result = await self._config_flow.valid_login(self.hass) errors = result.get("errors") else: _LOGGER.warning(f"{DEFAULT_NAME} ({host}) already configured") @@ -107,106 +60,52 @@ async def async_step_user(self, user_input=None): description_placeholders=user_input) if errors is None: - return self.async_create_entry(title=self.config_manager.data.host, data=user_input) + return self.async_create_entry(title=self._config_flow.config_data.host, data=user_input) + + data_schema = self._config_flow.get_default_data() - return self.async_show_form(step_id="user", data_schema=vol.Schema(CONFIG_FIELDS), errors=errors) + return self.async_show_form(step_id="user", data_schema=data_schema, errors=errors) async def async_step_import(self, info): """Import existing configuration from BlueIris.""" _LOGGER.debug(f"Starting async_step_import of {DOMAIN}") + title = f"{DEFAULT_NAME} (import from configuration.yaml)" - return self.async_create_entry( - title="BlueIris (import from configuration.yaml)", - data={ - CONF_HOST: info.get(CONF_HOST), - CONF_PORT: info.get(CONF_PORT, DEFAULT_PORT), - CONF_USERNAME: info.get(CONF_USERNAME, ""), - CONF_PASSWORD: info.get(CONF_PASSWORD, ""), - CONF_SSL: info.get(CONF_SSL) - }, - ) + return self.async_create_entry(title=title, data=info) -class BlueIrisOptionsFlowHandler(config_entries.OptionsFlow, BlueIrisConfigFlow): +class BlueIrisOptionsFlowHandler(config_entries.OptionsFlow): """Handle BlueIris options.""" def __init__(self, config_entry: ConfigEntry): """Initialize BlueIris options flow.""" super().__init__() - self.options = {} - self._data = {} - - for key in config_entry.options.keys(): - self.options[key] = config_entry.options[key] - - for key in config_entry.data.keys(): - self._data[key] = config_entry.data[key] + self._config_flow = ConfigFlowManager(config_entry) async def async_step_init(self, user_input=None): - """Manage the EdgeOS options.""" + """Manage the BlueIris options.""" return await self.async_step_blue_iris_additional_settings(user_input) - def get_value(self, key, default=None): - if default is None: - default = "" - - value = self._data.get(key, default) - - if key in self.options: - value = self.options.get(key, False) - - return value - async def async_step_blue_iris_additional_settings(self, user_input=None): errors = None - self.initialize(self.hass) + self._config_flow.initialize(self.hass) if user_input is not None: - clear_credentials = user_input.get(CONF_CLEAR_CREDENTIALS, False) - - if clear_credentials: - del user_input[CONF_USERNAME] - del user_input[CONF_PASSWORD] - else: - if CONF_PASSWORD in user_input: - password = user_input[CONF_PASSWORD] - user_input[CONF_PASSWORD] = self.password_manager.encrypt(password) - - self.update_config_data(self._data, user_input) + self._config_flow.update_options(user_input, True) - result = await self.valid_login(self.hass) + result = await self._config_flow.valid_login(self.hass) errors = result.get("errors") if errors is None: return self.async_create_entry(title="", data=user_input) - self.update_config_data(self._data, self.options) - config_data = self.config_manager.data - - options = { - CONF_USERNAME: config_data.username, - CONF_PASSWORD: config_data.password_clear_text, - CONF_EXCLUDE_SYSTEM_CAMERA: config_data.exclude_system_camera - } - - fields = {} - for option in options: - current_value = options[option] - obj_type = str - if option in [CONF_EXCLUDE_SYSTEM_CAMERA, CONF_CLEAR_CREDENTIALS]: - obj_type = bool - - fields[vol.Optional(option, default=current_value)] = obj_type - - fields[vol.Optional(CONF_CLEAR_CREDENTIALS, default=False)] = bool + data_schema = self._config_flow.get_default_options() return self.async_show_form( step_id="blue_iris_additional_settings", - data_schema=vol.Schema(fields), + data_schema=data_schema, errors=errors, - description_placeholders={ - CONF_HOST: config_data.host - } + description_placeholders=self._config_flow.data ) diff --git a/custom_components/blueiris/managers/config_flow_manager.py b/custom_components/blueiris/managers/config_flow_manager.py new file mode 100644 index 0000000..e9380b2 --- /dev/null +++ b/custom_components/blueiris/managers/config_flow_manager.py @@ -0,0 +1,168 @@ +import logging +from typing import Optional + +from homeassistant.config_entries import ConfigEntry + +from ..helpers.const import * +from ..api.blue_iris_api import BlueIrisApi + +from ..managers.configuration_manager import ConfigManager +from ..managers.password_manager import PasswordManager +from ..models.config_data import ConfigData + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlowManager: + config_manager: ConfigManager + password_manager: PasswordManager + options: Optional[dict] + data: Optional[dict] + config_entry: ConfigEntry + + def __init__(self, config_entry: Optional[ConfigEntry] = None): + self.config_entry = config_entry + + self.options = None + self.data = None + self._pre_config = False + + if config_entry is not None: + self._pre_config = True + + self.update_data(self.config_entry.data) + self.update_options(self.config_entry.options) + + self._is_initialized = True + self._auth_error = False + self._hass = None + + def initialize(self, hass): + self._hass = hass + + if not self._pre_config: + self.options = {} + self.data = {} + + self.password_manager = PasswordManager(self._hass) + self.config_manager = ConfigManager(self.password_manager) + + self._update_entry() + + @property + def config_data(self) -> ConfigData: + return self.config_manager.data + + def handle_password(self, user_input): + clear_credentials = user_input.get(CONF_CLEAR_CREDENTIALS, False) + + if clear_credentials: + del user_input[CONF_USERNAME] + del user_input[CONF_PASSWORD] + else: + if CONF_PASSWORD in user_input: + password_clear_text = user_input[CONF_PASSWORD] + password = self.password_manager.encrypt(password_clear_text) + + user_input[CONF_PASSWORD] = password + + def update_options(self, options: dict, update_entry: bool = False): + if options is not None: + if update_entry: + self.handle_password(options) + + new_options = {} + for key in options: + new_options[key] = options[key] + + self.options = new_options + else: + self.options = {} + + if update_entry: + self._update_entry() + + def update_data(self, data: dict, update_entry: bool = False): + new_data = None + + if data is not None: + if update_entry: + self.handle_password(data) + + new_data = {} + for key in data: + new_data[key] = data[key] + + self.data = new_data + + if update_entry: + self._update_entry() + + def _update_entry(self): + entry = ConfigEntry(0, "", "", self.data, "", "", {}, options=self.options) + + self.config_manager.update(entry) + + @staticmethod + def get_default_data(): + fields = { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT): int, + vol.Optional(CONF_SSL, default=False): bool, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + } + + data_schema = vol.Schema(fields) + + return data_schema + + def get_default_options(self): + config_data = self.config_data + + options = { + CONF_USERNAME: config_data.username, + CONF_PASSWORD: config_data.password_clear_text, + CONF_EXCLUDE_SYSTEM_CAMERA: config_data.exclude_system_camera, + CONF_CLEAR_CREDENTIALS: False + } + + fields = {} + for option in options: + current_value = options[option] + obj_type = str + if option in [CONF_EXCLUDE_SYSTEM_CAMERA, CONF_CLEAR_CREDENTIALS]: + obj_type = bool + + fields[vol.Optional(option, default=current_value)] = obj_type + + data_schema = vol.Schema(fields) + + return data_schema + + async def valid_login(self, hass): + errors = None + + config_data = self.config_manager.data + + api = BlueIrisApi(hass, self.config_manager) + await api.initialize() + + if not api.is_logged_in: + _LOGGER.warning(f"Failed to access BlueIris Server ({config_data.host})") + errors = { + "base": "invalid_server_details" + } + else: + has_credentials = config_data.has_credentials + + if has_credentials and not api.data.get("admin", False): + _LOGGER.warning(f"Failed to login BlueIris ({config_data.host}) due to invalid credentials") + errors = { + "base": "invalid_admin_credentials" + } + + return { + "logged-in": errors is None, + "errors": errors + }