1
0
mirror of https://github.com/natekspencer/hacs-oasis_mini.git synced 2025-11-14 08:03:52 -05:00

8 Commits
0.2.0 ... 0.3.0

Author SHA1 Message Date
Nathan Spencer
36da0249b7 Merge pull request #6 from natekspencer/dev
Code cleanup
2024-07-11 12:37:40 -06:00
Nathan Spencer
bcc8547e3e Code cleanup 2024-07-11 11:55:02 -06:00
Nathan Spencer
e678b20990 Merge pull request #5 from natekspencer/dev
Add ability to reconfigure integration to handle updated host ip
2024-07-11 11:54:12 -06:00
Nathan Spencer
cda435070d Add ability to reconfigure integration to handle updated host ip 2024-07-11 11:51:23 -06:00
Nathan Spencer
9b85d939c4 Merge pull request #4 from natekspencer/dev
Set HACS minimum HA version to 2024.4.0
2024-07-11 11:43:30 -06:00
Nathan Spencer
4eb86c5541 Set minimum HA version to 2024.4.0 2024-07-11 11:41:37 -06:00
Nathan Spencer
e35ae0d4fa Merge pull request #3 from natekspencer/dev
Add button to play a random track
2024-07-11 11:03:39 -06:00
Nathan Spencer
21105e497a Add button to play a random track 2024-07-11 11:00:01 -06:00
11 changed files with 162 additions and 75 deletions

View File

@@ -3,7 +3,7 @@
"name": "Home Assistant integration development",
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
"postCreateCommand": "sudo apt-get update && sudo apt-get install libturbojpeg0",
"postAttachCommand": ".devcontainer/setup",
"postAttachCommand": "scripts/setup",
"forwardPorts": [8123],
"customizations": {
"vscode": {

View File

@@ -2,7 +2,9 @@
from __future__ import annotations
from typing import Any, Coroutine
from dataclasses import dataclass
import random
from typing import Awaitable, Callable
from homeassistant.components.button import (
ButtonDeviceClass,
@@ -11,31 +13,68 @@ from homeassistant.components.button import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import OasisMiniCoordinator
from .entity import OasisMiniEntity
from .pyoasismini import OasisMini
from .pyoasismini.const import TRACKS
class OasisMiniButtonEntity(OasisMiniEntity, ButtonEntity):
"""Oasis Mini button entity."""
async def async_press(self) -> None:
"""Press the button."""
await self.device.async_reboot()
DESCRIPTOR = ButtonEntityDescription(
key="reboot", device_class=ButtonDeviceClass.RESTART
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Oasis Mini button using config entry."""
coordinator: OasisMiniCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([OasisMiniButtonEntity(coordinator, entry, DESCRIPTOR)])
async_add_entities(
[
OasisMiniButtonEntity(coordinator, entry, descriptor)
for descriptor in DESCRIPTORS
]
)
async def play_random_track(device: OasisMini) -> None:
"""Play random track."""
track = int(random.choice(list(TRACKS)))
if track not in device.playlist:
await device.async_add_track_to_playlist(track)
# Move track to next item in the playlist and then select it
if (idx := device.playlist.index(track)) != (next_idx := device.playlist_index + 1):
await device.async_move_track(idx, next_idx)
await device.async_change_track(next_idx)
await device.async_play()
@dataclass(frozen=True, kw_only=True)
class OasisMiniButtonEntityDescription(ButtonEntityDescription):
"""Oasis Mini button entity description."""
press_fn: Callable[[OasisMini], Awaitable[None]]
DESCRIPTORS = (
OasisMiniButtonEntityDescription(
key="reboot",
device_class=ButtonDeviceClass.RESTART,
press_fn=lambda device: device.async_reboot(),
),
OasisMiniButtonEntityDescription(
key="random_track",
name="Play random track",
press_fn=play_random_track,
),
)
class OasisMiniButtonEntity(OasisMiniEntity, ButtonEntity):
"""Oasis Mini button entity."""
entity_description: OasisMiniButtonEntityDescription
async def async_press(self) -> None:
"""Press the button."""
await self.entity_description.press_fn(self.device)
await self.coordinator.async_request_refresh()

View File

@@ -10,10 +10,9 @@ from aiohttp import ClientConnectorError
from httpx import ConnectError, HTTPStatusError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_HOST, CONF_PASSWORD
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaFlowError,
@@ -30,16 +29,14 @@ _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
OPTIONS_SCHEMA = vol.Schema(
{
vol.Optional(CONF_EMAIL): str,
vol.Optional(CONF_PASSWORD): str,
}
{vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
)
async def cloud_login(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Cloud login."""
coordinator: OasisMiniCoordinator = handler.parent_handler.hass.data[DOMAIN][
handler.parent_handler.config_entry.entry_id
]
@@ -49,8 +46,8 @@ async def cloud_login(
email=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD]
)
user_input[CONF_ACCESS_TOKEN] = coordinator.device.access_token
except:
raise SchemaFlowError("invalid_auth")
except Exception as ex:
raise SchemaFlowError("invalid_auth") from ex
del user_input[CONF_PASSWORD]
return user_input
@@ -75,7 +72,7 @@ class OasisMiniConfigFlow(ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
# async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult:
# async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> ConfigFlowResult:
# """Handle dhcp discovery."""
# self.host = discovery_info.ip
# self.name = discovery_info.hostname
@@ -85,13 +82,29 @@ class OasisMiniConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
) -> ConfigFlowResult:
"""Handle the initial step."""
return await self._async_step("user", STEP_USER_DATA_SCHEMA, user_input)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration."""
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
assert entry
suggested_values = user_input or entry.data
return await self._async_step(
"reconfigure", STEP_USER_DATA_SCHEMA, user_input, suggested_values
)
async def _async_step(
self, step_id: str, schema: vol.Schema, user_input: dict[str, Any] | None = None
) -> FlowResult:
self,
step_id: str,
schema: vol.Schema,
user_input: dict[str, Any] | None = None,
suggested_values: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle step setup."""
if abort := self._abort_if_configured(user_input):
return abort
@@ -108,19 +121,24 @@ class OasisMiniConfigFlow(ConfigFlow, domain=DOMAIN):
existing_entry, data=data
)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_abort(reason="reconfigure_successful")
return self.async_create_entry(
title=f"Oasis Mini {self.serial_number}",
data=data,
)
return self.async_show_form(step_id=step_id, data_schema=schema, errors=errors)
return self.async_show_form(
step_id=step_id,
data_schema=self.add_suggested_values_to_schema(schema, suggested_values),
errors=errors,
)
async def validate_client(self, user_input: dict[str, Any]) -> dict[str, str]:
"""Validate client setup."""
errors = {}
try:
async with asyncio.timeout(10):
client = create_client({"host": self.host} | user_input)
self.serial_number = await client.async_get_serial_number()
if not self.serial_number:
@@ -143,7 +161,7 @@ class OasisMiniConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def _abort_if_configured(
self, user_input: dict[str, Any] | None
) -> FlowResult | None:
) -> ConfigFlowResult | None:
"""Abort if configured."""
if self.host or user_input:
data = {CONF_HOST: self.host, **(user_input or {})}

View File

@@ -127,6 +127,11 @@ class OasisMini:
"""Return the url."""
return f"http://{self._host}/"
async def async_add_track_to_playlist(self, track: int) -> None:
"""Add track to playlist."""
await self._async_command(params={"ADDJOBLIST": track})
self.playlist.append(track)
async def async_change_track(self, index: int) -> None:
"""Change the track."""
if index >= len(self.playlist):
@@ -156,6 +161,10 @@ class OasisMini:
setattr(self, attr, value)
return status
async def async_move_track(self, _from: int, _to: int) -> None:
"""Move a track in the playlist."""
await self._async_command(params={"MOVEJOB": f"{_from};{_to}"})
async def async_pause(self) -> None:
"""Send pause command."""
await self._async_command(params={"CMDPAUSE": ""})
@@ -228,7 +237,7 @@ class OasisMini:
async def _async_get(self, **kwargs: Any) -> str | None:
"""Perform a GET request."""
response = await self._session.get(self.url, **kwargs)
if response.status == 200:
if response.status == 200 and response.content_type == "text/plain":
text = await response.text()
return text
return None
@@ -264,6 +273,19 @@ class OasisMini:
)
return response
async def async_cloud_get_tracks(self, tracks: list[int]) -> None:
"""Get cloud tracks."""
if not self.access_token:
return
response = await self._async_request(
"GET",
urljoin(CLOUD_BASE_URL, "api/track"),
headers={"Authorization": f"Bearer {self.access_token}"},
params={"ids[]": tracks},
)
return response
async def _async_request(self, method: str, url: str, **kwargs) -> Any:
"""Login via the cloud."""
response = await self._session.request(method, url, **kwargs)
@@ -283,3 +305,7 @@ class OasisMini:
self._current_track_details = await self.async_cloud_get_track_info(
self.current_track_id
)
async def async_get_playlist_details(self) -> dict:
"""Get playlist info."""
return await self.async_cloud_get_tracks(self.playlist)

View File

@@ -8,4 +8,4 @@ from typing import Final
__TRACKS_FILE = os.path.join(os.path.dirname(__file__), "tracks.json")
with open(__TRACKS_FILE, "r", encoding="utf8") as file:
TRACKS: Final[dict[int, dict[str, str]]] = json.load(file)
TRACKS: Final[dict[str, dict[str, str]]] = json.load(file)

View File

@@ -2,9 +2,6 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
@@ -18,27 +15,19 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import OasisMiniCoordinator
from .entity import OasisMiniEntity
from .pyoasismini import OasisMini
@dataclass(frozen=True, kw_only=True)
class OasisMiniSensorEntityDescription(SensorEntityDescription):
"""Oasis Mini sensor entity description."""
lookup_fn: Callable[[OasisMini], Any] | None = None
class OasisMiniSensorEntity(OasisMiniEntity, SensorEntity):
"""Oasis Mini sensor entity."""
entity_description: OasisMiniSensorEntityDescription | SensorEntityDescription
@property
def native_value(self) -> str | None:
"""Return the value reported by the sensor."""
if lookup_fn := getattr(self.entity_description, "lookup_fn", None):
return lookup_fn(self.device)
return getattr(self.device, self.entity_description.key)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Oasis Mini sensors using config entry."""
coordinator: OasisMiniCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[
OasisMiniSensorEntity(coordinator, entry, descriptor)
for descriptor in DESCRIPTORS
]
)
DESCRIPTORS = {
@@ -49,9 +38,7 @@ DESCRIPTORS = {
name="Download progress",
state_class=SensorStateClass.TOTAL_INCREASING,
),
}
OTHERS = {
} | {
SensorEntityDescription(
key=key,
name=key.replace("_", " ").capitalize(),
@@ -68,14 +55,10 @@ OTHERS = {
}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Oasis Mini sensors using config entry."""
coordinator: OasisMiniCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[
OasisMiniSensorEntity(coordinator, entry, descriptor)
for descriptor in DESCRIPTORS | OTHERS
]
)
class OasisMiniSensorEntity(OasisMiniEntity, SensorEntity):
"""Oasis Mini sensor entity."""
@property
def native_value(self) -> str | None:
"""Return the value reported by the sensor."""
return getattr(self.device, self.entity_description.key)

View File

@@ -6,6 +6,11 @@
"host": "[%key:common::config_flow::data::host%]"
}
},
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
},
"reauth_confirm": {
"data": {}
}
@@ -17,7 +22,9 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
}
},
"options": {

View File

@@ -6,6 +6,11 @@
"host": "Host"
}
},
"reconfigure": {
"data": {
"host": "Host"
}
},
"reauth_confirm": {
"data": {}
}
@@ -17,7 +22,9 @@
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured"
"already_configured": "Device is already configured",
"reauth_successful": "Re-authentication was successful",
"reconfigure_successful": "Re-configuration was successful"
}
},
"options": {

View File

@@ -1,6 +1,6 @@
{
"name": "Oasis Mini",
"homeassistant": "2024.7.0",
"homeassistant": "2024.4.0",
"render_readme": true,
"zip_release": true,
"filename": "oasis_mini.zip"

View File

@@ -4,3 +4,10 @@ known-first-party = ["homeassistant", "tests"]
forced-separate = ["tests"]
combine-as-imports = true
split-on-trailing-comma = false
[tool.pylint."MESSAGES CONTROL"]
# abstract-method - with intro of async there are always methods missing
disable = [
"abstract-method",
"unexpected-keyword-arg",
]