Skip to content

Commit

Permalink
0.7.7
Browse files Browse the repository at this point in the history
  • Loading branch information
gvigroux committed Jan 17, 2025
1 parent f1bb656 commit af6e04d
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 41 deletions.
25 changes: 5 additions & 20 deletions custom_components/freebox_home/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 9 additions & 0 deletions custom_components/freebox_home/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):

Expand Down
3 changes: 0 additions & 3 deletions custom_components/freebox_home/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/freebox_home/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 33 additions & 10 deletions custom_components/freebox_home/cover.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -94,6 +97,8 @@ def convert_state(self, state):
else:
return None



class FreeboxShutter(FreeboxBaseClass,CoverEntity):

def __init__(self, hass, router, node) -> None:
Expand All @@ -102,19 +107,38 @@ 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:
return CoverDeviceClass.SHUTTER

@property
def current_cover_position(self):
return self._state
return self._current_state

@property
def current_cover_tilt_position(self):
Expand All @@ -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."""
Expand All @@ -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))
2 changes: 1 addition & 1 deletion custom_components/freebox_home/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"after_dependencies": ["zeroconf"],
"zeroconf": ["_fbx-api._tcp.local."],
"codeowners": ["gvigroux"],
"version": "0.7.6"
"version": "0.7.7"
}
36 changes: 30 additions & 6 deletions custom_components/freebox_home/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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."""
Expand All @@ -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")
Expand Down
86 changes: 86 additions & 0 deletions custom_components/freebox_home/switch.py
Original file line number Diff line number Diff line change
@@ -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()
7 changes: 7 additions & 0 deletions custom_components/freebox_home/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,12 @@
"register_failed": "Failed to register, please try again",
"unknown": "Unexpected error"
}
},
"entity": {
"switch": {
"invert_switch": {
"name": "EN Mon Interrupteur Personnalisé"
}
}
}
}
7 changes: 7 additions & 0 deletions custom_components/freebox_home/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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é"
}
}
}
}

0 comments on commit af6e04d

Please sign in to comment.