diff --git a/custom_components/unfoldedcircle/__init__.py b/custom_components/unfoldedcircle/__init__.py index ed83e30..4cbf9c4 100644 --- a/custom_components/unfoldedcircle/__init__.py +++ b/custom_components/unfoldedcircle/__init__.py @@ -8,7 +8,7 @@ from pyUnfoldedCircleRemote.remote import UCRemote as remote -# from . import ucRemote as remote +#from . import ucRemote as remote from .const import DOMAIN @@ -19,6 +19,8 @@ Platform.SENSOR, Platform.BINARY_SENSOR, Platform.UPDATE, + Platform.BUTTON, + Platform.REMOTE, ] diff --git a/custom_components/unfoldedcircle/binary_sensor.py b/custom_components/unfoldedcircle/binary_sensor.py index 196b736..d7915ab 100644 --- a/custom_components/unfoldedcircle/binary_sensor.py +++ b/custom_components/unfoldedcircle/binary_sensor.py @@ -1,4 +1,4 @@ -"""Binary sensor platform for mobile_app.""" +"""Binary sensor platform for Unfolded Circle""" from typing import Any import logging @@ -6,6 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.device_registry import DeviceInfo from .const import DOMAIN from homeassistant.const import ( @@ -42,6 +43,22 @@ class BinarySensor(BinarySensorEntity): # https://developers.home-assistant.io/docs/core/entity/sensor device_class = ATTR_BATTERY_CHARGING + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self._remote.serial_number) + }, + name=self._remote.name, + manufacturer=self._remote.manufacturer, + model=self._remote.model_name, + sw_version=self._remote.sw_version, + hw_version=self._remote.hw_revision, + configuration_url=self._remote.configuration_url, + ) + def __init__(self, remote): """Initialize the sensor.""" self._remote = remote @@ -59,10 +76,4 @@ def is_on(self): return self._remote.is_charging async def async_update(self) -> None: - await self._remote.update() - - # async def async_restore_last_state(self, last_state): - # """Restore previous state.""" - - # await super().async_restore_last_state(last_state) - # self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON + await self._remote.update() diff --git a/custom_components/unfoldedcircle/button.py b/custom_components/unfoldedcircle/button.py new file mode 100644 index 0000000..308c357 --- /dev/null +++ b/custom_components/unfoldedcircle/button.py @@ -0,0 +1,73 @@ +"""Button for Unfolded Circle""" +from typing import Any +import logging + +from homeassistant.components.button import ButtonEntity, ButtonDeviceClass +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.const import EntityCategory +from homeassistant.helpers.device_registry import DeviceInfo +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + remote = hass.data[DOMAIN][config_entry.entry_id] + + # Verify that passed in configuration works + if not await remote.can_connect(): + _LOGGER.error("Could not connect to Remote") + return + + # Get Basic Device Information + await remote.update() + + new_devices = [] + new_devices.append(Button(remote)) + if new_devices: + async_add_entities(new_devices) + + +class Button(ButtonEntity): + """Representation of a Button entity.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_icon = "mdi:gesture-tap-button" + _attr_device_class = ButtonDeviceClass.RESTART + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self._remote.serial_number) + }, + name=self._remote.name, + manufacturer=self._remote.manufacturer, + model=self._remote.model_name, + sw_version=self._remote.sw_version, + hw_version=self._remote.hw_revision, + configuration_url=self._remote.configuration_url, + ) + + def __init__(self, remote): + """Initialize the sensor.""" + self._remote = remote + self._attr_unique_id = f"{self._remote.serial_number}_restart_button" + self._attr_name = f"{self._remote.name} Restart Remote" + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._remote._online + + async def async_press(self) -> None: + """Press the button.""" + await self._remote.post_system_command("RESTART") diff --git a/custom_components/unfoldedcircle/config_flow.py b/custom_components/unfoldedcircle/config_flow.py index 6c3021c..e81585f 100644 --- a/custom_components/unfoldedcircle/config_flow.py +++ b/custom_components/unfoldedcircle/config_flow.py @@ -20,13 +20,13 @@ from pyUnfoldedCircleRemote.remote import UCRemote -# from . import ucRemote +#from . import ucRemote from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -AUTH_APIKEY_NAME = "pyUnfoldedCircle" +AUTH_APIKEY_NAME = "pyUnfoldedCircle-dev" AUTH_USERNAME = "web-configurator" STEP_USER_DATA_SCHEMA = vol.Schema( diff --git a/custom_components/unfoldedcircle/manifest.json b/custom_components/unfoldedcircle/manifest.json index 98515f3..821ae77 100644 --- a/custom_components/unfoldedcircle/manifest.json +++ b/custom_components/unfoldedcircle/manifest.json @@ -4,12 +4,12 @@ "codeowners": ["@jackjpowell"], "config_flow": true, "dependencies": ["network"], - "version": "0.0.4", - "documentation": "https://github.com/jackjpowell/hass-unfoldedcircle", + "version": "0.1.0", + "documentation": "https://github.com/jackjpowell/hass_unfolded_circle", "homekit": {}, "integration_type": "device", "iot_class": "local_polling", - "requirements": ["pyUnfoldedCircleRemote==0.0.10"], + "requirements": ["pyUnfoldedCircleRemote==0.1.2"], "ssdp": [], "zeroconf": [] } diff --git a/custom_components/unfoldedcircle/remote.py b/custom_components/unfoldedcircle/remote.py new file mode 100644 index 0000000..995a351 --- /dev/null +++ b/custom_components/unfoldedcircle/remote.py @@ -0,0 +1,98 @@ +"""Remote sensor platform for Unfolded Circle""" +from typing import Any +import logging + +from homeassistant.components.remote import RemoteEntity, RemoteEntityFeature, RemoteEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity import ToggleEntityDescription +from homeassistant.helpers.device_registry import DeviceInfo +from collections.abc import Iterable +from .const import DOMAIN + +from homeassistant.const import ( + ATTR_BATTERY_CHARGING, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + remote = hass.data[DOMAIN][config_entry.entry_id] + + # Verify that passed in configuration works + if not await remote.can_connect(): + _LOGGER.error("Could not connect to Remote") + return + + # Get Basic Device Information + await remote.update() + await remote.get_remotes() + await remote.get_remote_codesets() + await remote.get_docks() + + new_devices = [] + new_devices.append(RemoteSensor(remote)) + if new_devices: + async_add_entities(new_devices) + + +class RemoteSensor(RemoteEntity): + # The class of this device. Note the value should come from the homeassistant.const + # module. More information on the available devices classes can be seen here: + # https://developers.home-assistant.io/docs/core/entity/sensor + _attr_icon = "mdi:remote" + entity_description: ToggleEntityDescription + _attr_supported_features: RemoteEntityFeature = RemoteEntityFeature.ACTIVITY + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self._remote.serial_number) + }, + name=self._remote.name, + manufacturer=self._remote.manufacturer, + model=self._remote.model_name, + sw_version=self._remote.sw_version, + hw_version=self._remote.hw_revision, + configuration_url=self._remote.configuration_url, + ) + + def __init__(self, remote): + """Initialize the sensor.""" + self._remote = remote + + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._remote.serial_number}_remote" + + # The name of the entity + self._attr_name = f"{self._remote.name} Remote" + self._attr_activity_list = [] + self._attr_is_on = False + for activity in self._remote.activities: + self._attr_activity_list.append(activity.name) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + self._attr_is_on = True + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + self._attr_is_on = False + + async def async_send_command(self, command: Iterable[str], **kwargs): + for indv_command in command: + await self._remote.send_remote_command( + device=kwargs.get("device"), + command=indv_command, + repeat=kwargs.get("num_repeats"), + ) diff --git a/custom_components/unfoldedcircle/sensor.py b/custom_components/unfoldedcircle/sensor.py index 39aadb5..dd405d4 100644 --- a/custom_components/unfoldedcircle/sensor.py +++ b/custom_components/unfoldedcircle/sensor.py @@ -10,9 +10,12 @@ DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, PERCENTAGE, - ATTR_BATTERY_CHARGING, + DATA_MEBIBYTES, ) +from homeassistant.const import EntityCategory +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import SensorStateClass from .const import DOMAIN @@ -38,6 +41,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): new_devices = [] new_devices.append(BatterySensor(remote)) new_devices.append(IlluminanceSensor(remote)) + new_devices.append(MemorySensor(remote)) + new_devices.append(StorageSensor(remote)) + new_devices.append(LoadSensor(remote)) if new_devices: async_add_entities(new_devices) @@ -57,9 +63,20 @@ def __init__(self, remote): # as name. If name is returned, this entity will then also become a device in the # HA UI. @property - def device_info(self): - """Return information to link this entity with the correct device.""" - return {"identifiers": {(DOMAIN, self._remote.serial_number)}} + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self._remote.serial_number) + }, + name=self._remote.name, + manufacturer=self._remote.manufacturer, + model=self._remote.model_name, + sw_version=self._remote.sw_version, + hw_version=self._remote.hw_revision, + configuration_url=self._remote.configuration_url, + ) # This property is important to let HA know if this entity is online or not. # If an entity is offline (return False), the UI will refelect this. @@ -159,3 +176,70 @@ def __init__(self, remote): def state(self): """Return the state of the sensor.""" return self._remote.ambient_light_intensity + + +class MemorySensor(SensorBase): + """Representation of a Sensor.""" + + device_class = DATA_MEBIBYTES + _attr_unit_of_measurement = "MiB" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__(self, remote): + """Initialize the sensor.""" + super().__init__(remote) + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._remote.serial_number}_memory_available" + + # The name of the entity + self._attr_name = f"{self._remote.name} Memory Available" + + @property + def state(self): + """Return the state of the sensor.""" + return self._remote.memory_available + + +class StorageSensor(SensorBase): + """Representation of a Sensor.""" + + device_class = DATA_MEBIBYTES + _attr_unit_of_measurement = "MiB" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__(self, remote): + """Initialize the sensor.""" + super().__init__(remote) + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._remote.serial_number}_storage_available" + + # The name of the entity + self._attr_name = f"{self._remote.name} Storage Available" + + @property + def state(self): + """Return the state of the sensor.""" + return self._remote.storage_available + +class LoadSensor(SensorBase): + """Representation of a Sensor.""" + + _attr_unit_of_measurement = "Load" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__(self, remote): + """Initialize the sensor.""" + super().__init__(remote) + # As per the sensor, this must be a unique value within this domain. This is done + # by using the device ID, and appending "_battery" + self._attr_unique_id = f"{self._remote.serial_number}_cpu_load_1_min" + + # The name of the entity + self._attr_name = f"{self._remote.name} CPU Load Avg (1 min)" + + @property + def state(self): + """Return the state of the sensor.""" + return self._remote._cpu_load.get("one") \ No newline at end of file diff --git a/custom_components/unfoldedcircle/switch.py b/custom_components/unfoldedcircle/switch.py index 674ea01..3f936b6 100644 --- a/custom_components/unfoldedcircle/switch.py +++ b/custom_components/unfoldedcircle/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +import typing import voluptuous as vol @@ -73,6 +74,8 @@ def device_info(self) -> DeviceInfo: manufacturer=self.switch.remote.manufacturer, model=self.switch.remote.model_name, sw_version=self.switch.remote.sw_version, + hw_version=self.switch.remote.hw_revision, + configuration_url=self.switch.remote.configuration_url, ) """Representation of a Switch.""" @@ -85,7 +88,7 @@ def __init__(self, switch) -> None: self._attr_unique_id = switch._id self._state = switch.state self.unique_id = self.switch._id - self._attr_icon = "mdi:remote" + self._attr_icon = "mdi:remote-tv" @property def name(self) -> str: diff --git a/custom_components/unfoldedcircle/update.py b/custom_components/unfoldedcircle/update.py index e87d8a3..62b9237 100644 --- a/custom_components/unfoldedcircle/update.py +++ b/custom_components/unfoldedcircle/update.py @@ -6,6 +6,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.const import EntityCategory +from homeassistant.helpers.device_registry import DeviceInfo from .const import DOMAIN from homeassistant.components.update.const import UpdateEntityFeature @@ -39,6 +41,24 @@ class Update(UpdateEntity): # module. More information on the available devices classes can be seen here: # https://developers.home-assistant.io/docs/core/entity/sensor + _attr_icon = "mdi:update" + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + identifiers={ + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self._remote.serial_number) + }, + name=self._remote.name, + manufacturer=self._remote.manufacturer, + model=self._remote.model_name, + sw_version=self._remote.sw_version, + hw_version=self._remote.hw_revision, + configuration_url=self._remote.configuration_url, + ) + def __init__(self, remote): """Initialize the sensor.""" self._remote = remote @@ -48,7 +68,7 @@ def __init__(self, remote): self._attr_unique_id = f"{self._remote.name}_update_status" # The name of the entity - self._attr_name = f"{self._remote.name} Update" + self._attr_name = f"{self._remote.name} Firmware" self._attr_auto_update = self._remote.automatic_updates self._attr_installed_version = self._remote.sw_version self._attr_device_class = "firmware" @@ -56,6 +76,7 @@ def __init__(self, remote): self._attr_latest_version = self._remote.latest_sw_version self._attr_release_url = self._remote.release_notes_url + self._attr_entity_category = EntityCategory.CONFIG # self._attr_state: None = None # _attr_release_summary = #TODO @@ -86,3 +107,5 @@ async def async_install( async def async_update(self) -> None: await self._remote.get_remote_update_information() + self._attr_latest_version = self._remote.latest_sw_version + self._attr_installed_version = self._remote.sw_version diff --git a/hacs.json b/hacs.json index eb80955..dd120b1 100644 --- a/hacs.json +++ b/hacs.json @@ -1,7 +1,7 @@ { "name": "Unfolded Circle", - "domains": ["binary_sensor", "sensor", "switch", "update"], - "homeassistant": "2023.11.0", + "domains": ["binary_sensor", "sensor", "switch", "update", "button", "remote"], + "homeassistant": "2023.10.0", "iot_class": "Local Polling", "render_readme": true, "zip_release": true,