diff --git a/custom_components/jibo/__init__.py b/custom_components/jibo/__init__.py index bd75788..aff53c0 100644 --- a/custom_components/jibo/__init__.py +++ b/custom_components/jibo/__init__.py @@ -1,52 +1,92 @@ -import asyncio import aiohttp import voluptuous as vol -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import logging -_LOGGER = logging.getLogger(__name__) from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + +_SAY_SCHEMA = vol.Schema({ + vol.Required("message"): str, + vol.Optional("robot"): str, +}) + + async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Jibo integration.""" - - async def handle_say_service(call): - message = call.data.get("message") - ip = hass.data[DOMAIN]["jibo_ip"] - url = f"http://{ip}:8089/tts_speak" - payload = { - "prompt": message, - "locale": "en-us", - "voice": "griffin", - "duration_stretch": 1, - "pitch": 3, - "pitchBandwidth": 0.4, - "mode": "text", - "outputMode": "stream", - "timeout": None, - "volume": 0, - "whisper": "FALSE", - "samplerate": 48000, - "postfilter": 0.4, - "framerate": 240, - "unvoicedvoiced": 0.35, - "allPass": 0.76, - "gvMCEP": 0.9, - "cached": "TRUE" - } - - async with aiohttp.ClientSession() as session: - try: - async with session.post(url, json=payload) as response: - if response.status != 200: - _LOGGER.error("Failed to speak: %s", await response.text()) - except aiohttp.ClientError as e: - _LOGGER.error("Error communicating with Jibo: %s", e) - - hass.services.async_register(DOMAIN, "say", handle_say_service) + hass.data.setdefault(DOMAIN, {}) return True -async def async_setup_entry(hass, entry): - """Store IP from config flow.""" - hass.data[DOMAIN] = {"jibo_ip": entry.data["jibo_ip"]} - return True \ No newline at end of file + +async def async_setup_entry(hass: HomeAssistant, entry): + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + "jibo_ip": entry.data["jibo_ip"], + "name": entry.data.get("name", entry.title), + } + + if not hass.services.has_service(DOMAIN, "say"): + async def handle_say(call: ServiceCall): + message = call.data["message"] + robot_filter = call.data.get("robot") + + targets = [ + data["jibo_ip"] + for data in hass.data[DOMAIN].values() + if robot_filter is None or data["name"] == robot_filter + ] + + if not targets: + _LOGGER.warning( + "No Jibo robot matched filter %r. Configured robots: %s", + robot_filter, + [d["name"] for d in hass.data[DOMAIN].values()], + ) + return + + payload = { + "prompt": message, + "locale": "en-us", + "voice": "griffin", + "duration_stretch": 1, + "pitch": 3, + "pitchBandwidth": 0.4, + "mode": "text", + "outputMode": "stream", + "timeout": None, + "volume": 0, + "whisper": "FALSE", + "samplerate": 48000, + "postfilter": 0.4, + "framerate": 240, + "unvoicedvoiced": 0.35, + "allPass": 0.76, + "gvMCEP": 0.9, + "cached": "TRUE", + } + + async with aiohttp.ClientSession() as session: + for ip in targets: + url = f"http://{ip}:8089/tts_speak" + try: + async with session.post(url, json=payload) as response: + if response.status != 200: + _LOGGER.error( + "Jibo at %s returned %s: %s", + ip, response.status, await response.text(), + ) + except aiohttp.ClientError as e: + _LOGGER.error("Error communicating with Jibo at %s: %s", ip, e) + + hass.services.async_register(DOMAIN, "say", handle_say, schema=_SAY_SCHEMA) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry): + hass.data[DOMAIN].pop(entry.entry_id, None) + + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, "say") + + return True diff --git a/custom_components/jibo/config_flow.py b/custom_components/jibo/config_flow.py index 5c0c4de..d68daf3 100644 --- a/custom_components/jibo/config_flow.py +++ b/custom_components/jibo/config_flow.py @@ -1,17 +1,36 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN + class JiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL async def async_step_user(self, user_input=None): + errors = {} + if user_input is not None: - return self.async_create_entry(title="Jibo", data=user_input) + ip = user_input["jibo_ip"].strip() + + await self.async_set_unique_id(ip) + self._abort_if_unique_id_configured() + + name = user_input.get("name", "").strip() or f"Jibo ({ip})" + + return self.async_create_entry( + title=name, + data={"jibo_ip": ip, "name": name}, + ) data_schema = vol.Schema({ - vol.Required("jibo_ip"): str + vol.Required("name"): str, + vol.Required("jibo_ip"): str, }) - return self.async_show_form(step_id="user", data_schema=data_schema) \ No newline at end of file + + return self.async_show_form( + step_id="user", + data_schema=data_schema, + errors=errors, + ) diff --git a/custom_components/jibo/manifest.json b/custom_components/jibo/manifest.json index c1d9f52..5b9b2cb 100644 --- a/custom_components/jibo/manifest.json +++ b/custom_components/jibo/manifest.json @@ -1,10 +1,11 @@ { "domain": "jibo", "name": "OpenJibo", - "version": "0.1.0.a", + "version": "0.1.0.alpha.2", "documentation": "https://jibohacks.zane.org/homeassistant/int", "requirements": [], "dependencies": [], "codeowners": ["@ZaneThePython"], - "config_flow": true + "config_flow": true, + "iot_class": "local_polling" } \ No newline at end of file diff --git a/custom_components/jibo/services.yaml b/custom_components/jibo/services.yaml index 08024f8..4b751d2 100644 --- a/custom_components/jibo/services.yaml +++ b/custom_components/jibo/services.yaml @@ -1,7 +1,17 @@ say: - description: Make Jibo Say Something + description: Make a Jibo robot say something. fields: message: - description: Text to speak - example: "Hello Home Assistant" - required: true \ No newline at end of file + description: The text to speak. + example: "Hello, Home Assistant!" + required: true + selector: + text: + robot: + description: > + Name of the robot to speak on. Leave empty to speak on all configured + Jibo robots. + example: "Living Room Jibo" + required: false + selector: + text: