diff --git a/custom_components/freebox_home/__init__.py b/custom_components/freebox_home/__init__.py index 0838f5e..2d0b694 100644 --- a/custom_components/freebox_home/__init__.py +++ b/custom_components/freebox_home/__init__.py @@ -19,31 +19,16 @@ async def async_setup(hass, config): return True -async def blocking_calls(hass, api, entry): - await api.open(entry.data[CONF_HOST], entry.data[CONF_PORT]) - - fbx_config = await api.system.get_config() - router = FreeboxRouter(hass, entry, api, fbx_config) - await router.update_all() - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.unique_id] = router - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - -async def test(hass, api, entry): - await api.open(entry.data[CONF_HOST], entry.data[CONF_PORT]) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Freebox component.""" - router = None try: - api = await get_api(hass, entry.data[CONF_HOST], entry.data[CONF_PORT]) - fbx_config = await api.system.get_config() - #await hass.async_add_executor_job(blocking_calls, hass, api, entry) - except: - _LOGGER.error("Unable to connect to the Freebox") + api = await get_api(hass, entry.data[CONF_HOST], entry.data[CONF_PORT]) + fbx_config = await api.system.get_config() + except Exception as e: + _LOGGER.error("Unable to connect to the Freebox: %s", repr(e)) + return False router = FreeboxRouter(hass, entry, api, fbx_config) await router.update_all() diff --git a/custom_components/freebox_home/binary_sensor.py b/custom_components/freebox_home/binary_sensor.py index 5b2d8c9..9f8ab67 100644 --- a/custom_components/freebox_home/binary_sensor.py +++ b/custom_components/freebox_home/binary_sensor.py @@ -22,6 +22,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(FreeboxPir(hass, router, node)) elif node["category"]=="dws": entities.append(FreeboxDws(hass, router, node)) + #elif node["category"]=="basic_shutter": + # entities.append(FreeboxCoverInverter(hass, router, node)) + #elif node["category"]=="shutter": + # entities.append(FreeboxCoverInverter(hass, router, node)) + #elif node["category"]=="opener": + # entities.append(FreeboxCoverInverter(hass, router, node)) + cover_node = next(filter(lambda x: (x["name"]=="cover" and x["ep_type"]=="signal"), node["show_endpoints"]), None) if( cover_node != None and cover_node.get("value", None) != None): @@ -30,6 +37,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) + + ''' Freebox motion detector sensor ''' class FreeboxPir(FreeboxBaseClass, BinarySensorEntity): diff --git a/custom_components/freebox_home/config_flow.py b/custom_components/freebox_home/config_flow.py index d99e3c7..bc924fa 100644 --- a/custom_components/freebox_home/config_flow.py +++ b/custom_components/freebox_home/config_flow.py @@ -89,10 +89,7 @@ async def async_step_link(self, user_input=None): ''' except AuthorizationError as error: - # Config file may be wrong, I will delete IT. _LOGGER.error("AuthorizationError: %s", error) - #await remove_config(self.hass, self._host) - #_LOGGER.error("The current configuration file is invalid. It has been deleted. Please retry") errors["base"] = "register_failed" except HttpRequestError: diff --git a/custom_components/freebox_home/const.py b/custom_components/freebox_home/const.py index 10d11c5..3c2ce4e 100644 --- a/custom_components/freebox_home/const.py +++ b/custom_components/freebox_home/const.py @@ -4,7 +4,7 @@ DOMAIN = "freebox_home" API_VERSION = "v8" -PLATFORMS = ["cover", "camera", "alarm_control_panel", "binary_sensor", "sensor"] +PLATFORMS = ["switch", "cover", "camera", "alarm_control_panel", "binary_sensor", "sensor", ] APP_DESC = { "app_id": "hass", diff --git a/custom_components/freebox_home/cover.py b/custom_components/freebox_home/cover.py index 5637354..931f634 100644 --- a/custom_components/freebox_home/cover.py +++ b/custom_components/freebox_home/cover.py @@ -1,11 +1,13 @@ """Support for Freebox covers.""" import logging import json +from homeassistant.util import slugify from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.cover import CoverEntity, CoverDeviceClass from .const import DOMAIN from .base_class import FreeboxBaseClass +from homeassistant.helpers.entity_registry import async_get from homeassistant.const import ( STATE_CLOSED, @@ -32,6 +34,7 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities, True) + class FreeboxBasicShutter(FreeboxBaseClass,CoverEntity): def __init__(self, hass, router, node) -> None: @@ -94,6 +97,8 @@ def convert_state(self, state): else: return None + + class FreeboxShutter(FreeboxBaseClass,CoverEntity): def __init__(self, hass, router, node) -> None: @@ -102,11 +107,30 @@ def __init__(self, hass, router, node) -> None: self._command_position = self.get_command_id(node['type']['endpoints'], "slot", "position_set") self._command_up = self.get_command_id(node['type']['endpoints'], "slot", "position_set") self._command_down = self.get_command_id(node['type']['endpoints'], "slot", "position_set") - self._command_stop = self.get_command_id(node['show_endpoints'], "slot", "stop") + self._command_stop = self.get_command_id(node['show_endpoints'], "slot", "stop") self._command_toggle = self.get_command_id(node['show_endpoints'], "slot", "toggle") self._command_state = self.get_command_id(node['type']['endpoints'], "signal", "position_set") - self._state = self.get_node_value(node['show_endpoints'], "signal", "state") - self._attr_device_class = CoverDeviceClass.SHUTTER + self._current_state = self.get_node_value(node['show_endpoints'], "signal", "state") + + # Go over all entities to find the switch + self._invert_entity_id = None + entity_registry = async_get(hass) + for entity in entity_registry.entities.values(): + if (entity.unique_id == self.unique_id + "_InvertSwitch"): + self._invert_entity_id = entity.entity_id + + def get_corrected_state(self, value): + if(self._invert_entity_id == None): + return value + state = self._hass.states.get(self._invert_entity_id) + if( value == None ): + return None + if( state == None ): + return None + if( state.state == "on" ): + return 100 - value + return value + @property def device_class(self) -> str: @@ -114,7 +138,7 @@ def device_class(self) -> str: @property def current_cover_position(self): - return self._state + return self._current_state @property def current_cover_tilt_position(self): @@ -123,22 +147,21 @@ def current_cover_tilt_position(self): @property def is_closed(self): """Return if the cover is closed or not.""" - if(self._state == 100): + if(self._current_state == 0): return True return False async def async_set_cover_position(self, position, **kwargs): """Set cover position.""" - await self.set_home_endpoint_value(self._command_position, {"value": position}) - self._state = STATE_OPEN + await self.set_home_endpoint_value(self._command_position, {"value": self.get_corrected_state(self._current_state)}) async def async_open_cover(self, **kwargs): """Open cover.""" - await self.set_home_endpoint_value(self._command_up, {"value": 0}) + await self.set_home_endpoint_value(self._command_up, {"value": self.get_corrected_state(self._current_state)}) async def async_close_cover(self, **kwargs): """Close cover.""" - await self.set_home_endpoint_value(self._command_down, {"value": 100}) + await self.set_home_endpoint_value(self._command_down, {"value": self.get_corrected_state(self._current_state)}) async def async_stop_cover(self, **kwargs): """Stop cover.""" @@ -149,4 +172,4 @@ async def async_update(self): """Get the state & name and update it.""" node = self._router.nodes[self._id]; self._name = node["label"].strip() - self._state = await self.get_home_endpoint_value(self._command_state) + self._current_state = self.get_corrected_state(await self.get_home_endpoint_value(self._command_state)) diff --git a/custom_components/freebox_home/manifest.json b/custom_components/freebox_home/manifest.json index 3dea224..eaf86c8 100644 --- a/custom_components/freebox_home/manifest.json +++ b/custom_components/freebox_home/manifest.json @@ -9,5 +9,5 @@ "after_dependencies": ["zeroconf"], "zeroconf": ["_fbx-api._tcp.local."], "codeowners": ["gvigroux"], - "version": "0.7.6" + "version": "0.7.7" } \ No newline at end of file diff --git a/custom_components/freebox_home/router.py b/custom_components/freebox_home/router.py index 6979c5b..717f72b 100644 --- a/custom_components/freebox_home/router.py +++ b/custom_components/freebox_home/router.py @@ -2,6 +2,8 @@ from datetime import datetime, timedelta import logging import os +import asyncio +import json from pathlib import Path from typing import Any, Dict, Optional @@ -43,6 +45,8 @@ async def update_all(self, now: Optional[datetime] = None) -> None: """Update all nodes""" try: fbx_nodes: Dict[str, Any] = await self._api.home.get_home_nodes() + + except InsufficientPermissionsError as error: _LOGGER.error("InsufficientPermissionsError: You need to browse http://mafreebox.freebox.fr/#Fbx.os.app.settings.Accounts and grant the access policy: \"Gestion de l'alarme et maison connectée\"") return @@ -53,6 +57,8 @@ async def update_all(self, now: Optional[datetime] = None) -> None: continue self.nodes[fbx_node["id"]] = fbx_node + #fbx_node = json.loads('{"adapter":0,"area":29,"category":"shutter","group":{"label":"Chambre"},"id":25,"label":"Volet Chambre","name":"node_25","props":{"Address":5187680,"ArcId":9},"show_endpoints":[{"category":"","ep_type":"slot","id":0,"label":"Consigne d\'ouverture","name":"position_set","ui":{"access":"w","display":"slider","icon_url":"/resources/images/home/pictos/volet_3.png","range":[0,100],"unit":"%"},"value":0,"value_type":"int","visibility":"normal"},{"category":"","ep_type":"slot","id":1,"label":"Stop","name":"stop","ui":{"access":"w","display":"button"},"value":null,"value_type":"void","visibility":"normal"},{"category":"","ep_type":"slot","id":2,"label":"Toggle","name":"toggle","ui":{"access":"w","display":"button"},"value":null,"value_type":"void","visibility":"normal"},{"category":"","ep_type":"signal","id":4,"label":"Consigne d\'ouverture","name":"position_set","refresh":2000,"ui":{"access":"r","display":"slider","icon_url":"/resources/images/home/pictos/volet_3.png","range":[0,100],"unit":"%"},"value":0,"value_type":"int","visibility":"normal"},{"category":"","ep_type":"signal","id":5,"label":"État","name":"state","refresh":2000,"ui":{"access":"r","display":"text"},"value":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","value_type":"string","visibility":"normal"}],"signal_links":[],"slot_links":[],"status":"active","type":{"abstract":false,"endpoints":[{"ep_type":"slot","id":0,"label":"Consigne d\'ouverture","name":"position_set","value_type":"int","visiblity":"normal"},{"ep_type":"slot","id":1,"label":"Stop","name":"stop","value_type":"void","visiblity":"normal"},{"ep_type":"slot","id":2,"label":"Toggle","name":"toggle","value_type":"void","visiblity":"normal"},{"ep_type":"slot","id":3,"label":"Consigne d\'ouverture","name":"position","value_type":"int","visiblity":"normal"},{"ep_type":"signal","id":4,"label":"Consigne d\'ouverture","name":"position_set","param_type":"void","value_type":"int","visiblity":"normal"},{"ep_type":"signal","id":5,"label":"État","name":"state","param_type":"void","value_type":"string","visiblity":"normal"}],"generic":false,"icon":"/resources/images/home/pictos/volet_3.png","inherit":"node::ios","label":"Volet roulant","name":"node::ios::2","params":{},"physical":true}}') + #self.nodes[fbx_node["id"]] = fbx_node async def close(self) -> None: """Close the connection.""" @@ -62,18 +68,36 @@ async def close(self) -> None: self._api = None -async def get_api(hass, host: str, port, retry = 0): - """Get the Freebox API.""" - freebox_path = Store(hass, STORAGE_VERSION, STORAGE_KEY).path + +async def async_get_path(hass, name): + freebox_path = Store(hass, STORAGE_VERSION, STORAGE_KEY).path if not os.path.exists(freebox_path): await hass.async_add_executor_job(os.makedirs, freebox_path) + return Path(f"{freebox_path}/{slugify(name)}.conf") + +def get_path(hass, name): + freebox_path = Store(hass, STORAGE_VERSION, STORAGE_KEY).path + return Path(f"{freebox_path}/{slugify(name)}.conf") - token_file = Path(f"{freebox_path}/{slugify(host)}.conf") - api = Freepybox(APP_DESC, token_file, api_version="latest") +async def get_api(hass, host: str, port, retry = 0): + """Get the Freebox API.""" + + path = await async_get_path(hass, host) + api = Freepybox(APP_DESC, path, api_version="latest") + try: - await api.open(host, port) + await api.open(host, port) + #loop = asyncio.get_running_loop() + #await loop.run_in_executor(None, async_func_wrapper, api, host, port) + + #loop = asyncio.new_event_loop() + #fetches = [api.open(host, port)] + #results = await asyncio.gather(*fetches) + #res = await asyncio.create_task(api.open(host, port)) + #result = await hass.async_add_executor_job(async_func_wrapper, api, host, port) + await api.system.get_config() except AuthorizationError as error: _LOGGER.error("AuthorizationError: Please accept the application authorization on your Freebox screen") diff --git a/custom_components/freebox_home/switch.py b/custom_components/freebox_home/switch.py new file mode 100644 index 0000000..a6ae32f --- /dev/null +++ b/custom_components/freebox_home/switch.py @@ -0,0 +1,86 @@ +import logging +from dataclasses import dataclass +from typing import Any +import os + +from .const import DOMAIN +from .base_class import FreeboxBaseClass + +from homeassistant.util import slugify +from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.components.switch import SwitchEntityDescription, SwitchEntity + +from .router import (get_path) + +_LOGGER = logging.getLogger(__name__) + + + + +async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities) -> None: + + router = hass.data[DOMAIN][entry.unique_id] + + entities = [] + for nodeId, node in router.nodes.items(): + #if node["category"]=="basic_shutter": + # entities.append(FreeboxShutterInvertSwitchEntity(hass, router, node)) + if node["category"]=="shutter": + entities.append(FreeboxShutterInvertSwitchEntity(hass, router, node)) + elif node["category"]=="opener": + entities.append(FreeboxShutterInvertSwitchEntity(hass, router, node)) + + async_add_entities(entities, True) + + + + +class FreeboxShutterInvertSwitchEntity(FreeboxBaseClass, SwitchEntity): + _attr_has_entity_name = True + + def __init__(self, hass, router, node): + super().__init__(hass, router, node) + + self._unique_id = f"{self._router.mac}-node_{self._id}" + '_InvertSwitch' + self._attr_icon = "mdi:directions-fork" + self._name = "Inverser commandes" + + self._state = False + self._path = get_path(hass, self._unique_id + '_InvertSwitch') + + try: + value = self._path.read_text() + if( value == "1"): + self._state = True + except OSError as e: + return + + + @property + def translation_key(self): + return "invert_switch" + + @property + def is_on(self) -> bool | None: + """Return True if entity is on.""" + return self._state + + async def async_turn_on(self, **kwargs: Any) -> None: + self._path.write_text('1') + self._state = True + + async def async_turn_off(self, **kwargs: Any) -> None: + self._path.write_text('0') + self._state = False + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return True + + @callback + def _handle_coordinator_update(self, update: bool = True) -> None: + self._attr_is_on = self.is_on + if update: + self.async_write_ha_state() diff --git a/custom_components/freebox_home/translations/en.json b/custom_components/freebox_home/translations/en.json index 4e9da1a..24f2566 100644 --- a/custom_components/freebox_home/translations/en.json +++ b/custom_components/freebox_home/translations/en.json @@ -26,5 +26,12 @@ "register_failed": "Failed to register, please try again", "unknown": "Unexpected error" } + }, + "entity": { + "switch": { + "invert_switch": { + "name": "EN Mon Interrupteur Personnalisé" + } + } } } \ No newline at end of file diff --git a/custom_components/freebox_home/translations/fr.json b/custom_components/freebox_home/translations/fr.json index 9ff2da6..fb7733c 100644 --- a/custom_components/freebox_home/translations/fr.json +++ b/custom_components/freebox_home/translations/fr.json @@ -26,5 +26,12 @@ "register_failed": "\u00c9chec de l'inscription, veuillez r\u00e9essayer", "unknown": "Erreur inconnue: veuillez r\u00e9essayer plus tard" } + }, + "entity": { + "switch": { + "invert_switch": { + "name": "FR Mon Interrupteur Personnalisé" + } + } } } \ No newline at end of file