mirror of
https://github.com/natekspencer/hacs-oasis_mini.git
synced 2025-11-16 09:03:50 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82a6a3cc1d | ||
|
|
e2f5727669 | ||
|
|
8650fd597a | ||
|
|
7bef2cbe3b | ||
|
|
5ea472821b | ||
|
|
ab09bde752 | ||
|
|
f49b8ce1d2 | ||
|
|
cbbe8bc10d | ||
|
|
c2c62bb875 | ||
|
|
108b1850b7 | ||
|
|
ffc74a9dcb | ||
|
|
f67aee166a | ||
|
|
4ed6b1701d | ||
|
|
ade3e7c666 | ||
|
|
4c112f2b06 | ||
|
|
f850158a8e | ||
|
|
8bb8cf9447 | ||
|
|
1c8b2f052c | ||
|
|
73f96d8302 | ||
|
|
9cc1d6d314 | ||
|
|
4894e3549d | ||
|
|
221f314dd6 | ||
|
|
595621652a | ||
|
|
42040895e2 | ||
|
|
51c4c8a6a2 | ||
|
|
ddabccc4a8 | ||
|
|
94860106ea |
@@ -1,8 +1,8 @@
|
||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
||||
{
|
||||
"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",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.13-bookworm",
|
||||
"postCreateCommand": "scripts/setup",
|
||||
"postAttachCommand": "scripts/setup",
|
||||
"forwardPorts": [8123],
|
||||
"customizations": {
|
||||
@@ -26,7 +26,10 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "always"
|
||||
},
|
||||
"files.trimTrailingWhitespace": true
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
4
.github/workflows/update-tracks.yml
vendored
4
.github/workflows/update-tracks.yml
vendored
@@ -12,10 +12,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python 3.12
|
||||
- name: Set up Python 3.13
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.13"
|
||||
- name: Install dependencies
|
||||
run: pip install homeassistant
|
||||
- name: Update tracks
|
||||
|
||||
10
.pre-commit-config.yaml
Normal file
10
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.9.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
14
README.md
14
README.md
@@ -56,6 +56,20 @@ Alternatively:
|
||||
|
||||
After this integration is set up, you can configure the integration to connect to the Kinetic Oasis cloud API. This will allow pulling in certain details (such as track name and image) that are otherwise not available.
|
||||
|
||||
# Actions
|
||||
|
||||
The media player entity supports various actions, including managing the playlist queue. You can specify a track by its ID or name. If using a track name, it must match an entry in the [tracks list](custom_components/oasis_mini/pyoasismini/tracks.json). To specify multiple tracks, separate them with commas. An example is below:
|
||||
|
||||
```yaml
|
||||
action: media_player.play_media
|
||||
target:
|
||||
entity_id: media_player.oasis_mini
|
||||
data:
|
||||
media_content_id: 63, Turtle
|
||||
media_content_type: track
|
||||
enqueue: replace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support Me
|
||||
|
||||
@@ -4,6 +4,7 @@ automation:
|
||||
dhcp:
|
||||
frontend:
|
||||
history:
|
||||
isal:
|
||||
logbook:
|
||||
media_source:
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type OasisMiniConfigEntry = ConfigEntry[OasisMiniCoordinator]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.IMAGE,
|
||||
Platform.LIGHT,
|
||||
|
||||
55
custom_components/oasis_mini/binary_sensor.py
Normal file
55
custom_components/oasis_mini/binary_sensor.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Oasis Mini binary sensor entity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import OasisMiniConfigEntry
|
||||
from .coordinator import OasisMiniCoordinator
|
||||
from .entity import OasisMiniEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: OasisMiniConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Oasis Mini sensors using config entry."""
|
||||
coordinator: OasisMiniCoordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
OasisMiniBinarySensorEntity(coordinator, descriptor)
|
||||
for descriptor in DESCRIPTORS
|
||||
)
|
||||
|
||||
|
||||
DESCRIPTORS = {
|
||||
BinarySensorEntityDescription(
|
||||
key="busy",
|
||||
translation_key="busy",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="wifi_connected",
|
||||
translation_key="wifi_status",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class OasisMiniBinarySensorEntity(OasisMiniEntity, BinarySensorEntity):
|
||||
"""Oasis Mini binary sensor entity."""
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return getattr(self.device, self.entity_description.key)
|
||||
@@ -58,7 +58,7 @@ DESCRIPTORS = (
|
||||
),
|
||||
OasisMiniButtonEntityDescription(
|
||||
key="random_track",
|
||||
name="Play random track",
|
||||
translation_key="random_track",
|
||||
press_fn=play_random_track,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
|
||||
|
||||
from .pyoasismini import OasisMini
|
||||
from .pyoasismini import TRACKS, OasisMini
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_client(data: dict[str, Any]) -> OasisMini:
|
||||
@@ -27,3 +30,21 @@ async def add_and_play_track(device: OasisMini, track: int) -> None:
|
||||
|
||||
if device.status_code != 4:
|
||||
await device.async_play()
|
||||
|
||||
|
||||
def get_track_id(track: str) -> int | None:
|
||||
"""Get a track id.
|
||||
|
||||
`track` can be either an id or title
|
||||
"""
|
||||
track = track.lower().strip()
|
||||
if track not in map(str, TRACKS):
|
||||
track = next(
|
||||
(id for id, info in TRACKS.items() if info["name"].lower() == track), track
|
||||
)
|
||||
|
||||
try:
|
||||
return int(track)
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid track: %s", track)
|
||||
return None
|
||||
|
||||
45
custom_components/oasis_mini/icons.json
Normal file
45
custom_components/oasis_mini/icons.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"wifi_status": {
|
||||
"default": "mdi:wifi",
|
||||
"state": {
|
||||
"off": "mdi:wifi-off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"download_progress": {
|
||||
"default": "mdi:progress-download"
|
||||
},
|
||||
"drawing_progress": {
|
||||
"default": "mdi:progress-pencil"
|
||||
},
|
||||
"error": {
|
||||
"default": "mdi:alert-circle-outline",
|
||||
"state": {
|
||||
"0": "mdi:circle-outline"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"state": {
|
||||
"booting": "mdi:loading",
|
||||
"centering": "mdi:record-circle-outline",
|
||||
"downloading": "mdi:progress-download",
|
||||
"error": "mdi:alert-circle-outline",
|
||||
"live": "mdi:pencil-circle-outline",
|
||||
"paused": "mdi:motion-pause-outline",
|
||||
"playing": "mdi:motion-play-outline",
|
||||
"stopped": "mdi:stop-circle-outline",
|
||||
"updating": "mdi:update"
|
||||
}
|
||||
},
|
||||
"wifi_connected": {
|
||||
"default": "mdi:wifi",
|
||||
"state": {
|
||||
"off": "mdi:wifi-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,10 @@ class OasisMiniImageEntity(OasisMiniEntity, ImageEntity):
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if self._track_id != self.device.track_id or (
|
||||
self._progress != self.device.progress and self.device.access_token
|
||||
):
|
||||
if (
|
||||
self._track_id != self.device.track_id
|
||||
or (self._progress != self.device.progress and self.device.access_token)
|
||||
) and (self.device.status == "playing" or self._cached_image is None):
|
||||
self._attr_image_last_updated = self.coordinator.last_updated
|
||||
self._track_id = self.device.track_id
|
||||
self._progress = self.device.progress
|
||||
|
||||
@@ -106,7 +106,7 @@ class OasisMiniLightEntity(OasisMiniEntity, LightEntity):
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
DESCRIPTOR = LightEntityDescription(key="led", name="LED")
|
||||
DESCRIPTOR = LightEntityDescription(key="led", translation_key="led")
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -19,7 +19,9 @@ from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import OasisMiniConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .entity import OasisMiniEntity
|
||||
from .helpers import get_track_id
|
||||
from .pyoasismini.const import TRACKS
|
||||
|
||||
|
||||
@@ -102,18 +104,30 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
return MediaPlayerState.ON
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
def abort_if_busy(self) -> None:
|
||||
"""Abort if the device is currently busy."""
|
||||
if self.device.busy:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_busy",
|
||||
translation_placeholders={"name": self._friendly_name_internal()},
|
||||
)
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
self.abort_if_busy()
|
||||
await self.device.async_pause()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
self.abort_if_busy()
|
||||
await self.device.async_play()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
self.abort_if_busy()
|
||||
await self.device.async_stop()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@@ -127,6 +141,7 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
self.abort_if_busy()
|
||||
if (index := self.device.playlist_index - 1) < 0:
|
||||
index = len(self.device.playlist) - 1
|
||||
await self.device.async_change_track(index)
|
||||
@@ -134,6 +149,7 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
self.abort_if_busy()
|
||||
if (index := self.device.playlist_index + 1) >= len(self.device.playlist):
|
||||
index = 0
|
||||
await self.device.async_change_track(index)
|
||||
@@ -147,32 +163,35 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Play a piece of media."""
|
||||
if media_id not in map(str, TRACKS):
|
||||
media_id = next(
|
||||
(
|
||||
id
|
||||
for id, info in TRACKS.items()
|
||||
if info["name"].lower() == media_id.lower()
|
||||
),
|
||||
media_id,
|
||||
self.abort_if_busy()
|
||||
if media_type == MediaType.PLAYLIST:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN, translation_key="playlists_unsupported"
|
||||
)
|
||||
try:
|
||||
track = int(media_id)
|
||||
except ValueError as err:
|
||||
raise ServiceValidationError(f"Invalid media: {media_id}") from err
|
||||
else:
|
||||
track = list(filter(None, map(get_track_id, media_id.split(","))))
|
||||
if not track:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_media",
|
||||
translation_placeholders={"media": media_id},
|
||||
)
|
||||
|
||||
device = self.device
|
||||
enqueue = MediaPlayerEnqueue.NEXT if not enqueue else enqueue
|
||||
if enqueue == MediaPlayerEnqueue.REPLACE:
|
||||
await device.async_set_playlist([track])
|
||||
await device.async_set_playlist(track)
|
||||
else:
|
||||
await device.async_add_track_to_playlist(track)
|
||||
|
||||
if enqueue in (MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY):
|
||||
# Move track to next item in the playlist
|
||||
if (index := (len(device.playlist) - 1)) != device.playlist_index:
|
||||
new_tracks = 1 if isinstance(track, int) else len(track)
|
||||
if (index := (len(device.playlist) - new_tracks)) != device.playlist_index:
|
||||
if index != (
|
||||
_next := min(device.playlist_index + 1, len(device.playlist) - 1)
|
||||
_next := min(
|
||||
device.playlist_index + 1, len(device.playlist) - new_tracks
|
||||
)
|
||||
):
|
||||
await device.async_move_track(index, _next)
|
||||
if enqueue == MediaPlayerEnqueue.PLAY:
|
||||
@@ -188,6 +207,7 @@ class OasisMiniMediaPlayerEntity(OasisMiniEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_clear_playlist(self) -> None:
|
||||
"""Clear players playlist."""
|
||||
self.abort_if_busy()
|
||||
await self.device.async_clear_playlist()
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
@@ -35,14 +35,14 @@ class OasisMiniNumberEntity(OasisMiniEntity, NumberEntity):
|
||||
DESCRIPTORS = {
|
||||
NumberEntityDescription(
|
||||
key="ball_speed",
|
||||
name="Ball speed",
|
||||
translation_key="ball_speed",
|
||||
mode=NumberMode.SLIDER,
|
||||
native_max_value=BALL_SPEED_MAX,
|
||||
native_min_value=BALL_SPEED_MIN,
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key="led_speed",
|
||||
name="LED speed",
|
||||
translation_key="led_speed",
|
||||
mode=NumberMode.SLIDER,
|
||||
native_max_value=LED_SPEED_MAX,
|
||||
native_min_value=LED_SPEED_MIN,
|
||||
|
||||
@@ -8,10 +8,9 @@ from typing import Any, Awaitable, Final
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from aiohttp import ClientResponseError, ClientSession
|
||||
import async_timeout
|
||||
|
||||
from .const import TRACKS
|
||||
from .utils import _bit_to_bool, decrypt_svg_content
|
||||
from .utils import _bit_to_bool, _parse_int, decrypt_svg_content
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,8 +54,8 @@ LED_EFFECTS: Final[dict[str, str]] = {
|
||||
|
||||
CLOUD_BASE_URL = "https://app.grounded.so"
|
||||
|
||||
BALL_SPEED_MAX: Final = 1000
|
||||
BALL_SPEED_MIN: Final = 200
|
||||
BALL_SPEED_MAX: Final = 400
|
||||
BALL_SPEED_MIN: Final = 100
|
||||
LED_SPEED_MAX: Final = 90
|
||||
LED_SPEED_MIN: Final = -90
|
||||
|
||||
@@ -159,17 +158,17 @@ class OasisMini:
|
||||
"""Return the url."""
|
||||
return f"http://{self._host}/"
|
||||
|
||||
async def async_add_track_to_playlist(self, track: int) -> None:
|
||||
async def async_add_track_to_playlist(self, track: int | list[int]) -> None:
|
||||
"""Add track to playlist."""
|
||||
if not track:
|
||||
return
|
||||
|
||||
if isinstance(track, int):
|
||||
track = [track]
|
||||
if 0 in self.playlist:
|
||||
playlist = [t for t in self.playlist if t] + [track]
|
||||
playlist = [t for t in self.playlist if t] + track
|
||||
return await self.async_set_playlist(playlist)
|
||||
|
||||
await self._async_command(params={"ADDJOBLIST": track})
|
||||
self.playlist.append(track)
|
||||
self.playlist.extend(track)
|
||||
|
||||
async def async_change_track(self, index: int) -> None:
|
||||
"""Change the track."""
|
||||
@@ -210,25 +209,29 @@ class OasisMini:
|
||||
raw_status = await self._async_get(params={"GETSTATUS": ""})
|
||||
_LOGGER.debug("Status: %s", raw_status)
|
||||
values = raw_status.split(";")
|
||||
playlist = [int(track) for track in values[3].split(",") if track]
|
||||
playlist = [_parse_int(track) for track in values[3].split(",") if track]
|
||||
shift = len(values) - 18 if len(values) > 17 else 0
|
||||
status = {
|
||||
"status_code": int(values[0]), # see status code map
|
||||
"error": int(values[1]), # noqa: E501; error, 0 = none, and 10 = ?, 18 = can't download?
|
||||
"ball_speed": int(values[2]), # 200 - 1000
|
||||
"status_code": _parse_int(values[0]), # see status code map
|
||||
"error": _parse_int(values[1]), # noqa: E501; error, 0 = none, and 10 = ?, 18 = can't download?
|
||||
"ball_speed": _parse_int(values[2]), # 200 - 1000
|
||||
"playlist": playlist,
|
||||
"playlist_index": min(int(values[4]), len(playlist)), # index of above
|
||||
"progress": int(values[5]), # 0 - max svg path
|
||||
"playlist_index": min(_parse_int(values[4]), len(playlist)), # noqa: E501; index of above
|
||||
"progress": _parse_int(values[5]), # 0 - max svg path
|
||||
"led_effect": values[6], # led effect (code lookup)
|
||||
"led_color_id": values[7], # led color id?
|
||||
"led_speed": int(values[8]), # -90 - 90
|
||||
"brightness": int(values[9]) if values[10] else 0, # noqa: E501; 0 - 200 in app, but seems to be 0 (off) to 304 (max), then repeats
|
||||
"color": values[10] or None, # hex color code
|
||||
"busy": _bit_to_bool(values[11]), # noqa: E501; device is busy (downloading track, centering, software update)?
|
||||
"download_progress": int(values[12]),
|
||||
"max_brightness": int(values[13]),
|
||||
"wifi_connected": _bit_to_bool(values[14]),
|
||||
"repeat_playlist": _bit_to_bool(values[15]),
|
||||
"autoplay": AUTOPLAY_MAP.get(values[16]),
|
||||
"led_speed": _parse_int(values[8]), # -90 - 90
|
||||
"brightness": _parse_int(values[9]), # noqa: E501; 0 - 200 in app, but seems to be 0 (off) to 304 (max), then repeats
|
||||
"color": values[10] if "#" in values[10] else None, # hex color code
|
||||
"busy": _bit_to_bool(values[11 + shift]), # noqa: E501; device is busy (downloading track, centering, software update)?
|
||||
"download_progress": _parse_int(values[12 + shift]),
|
||||
"max_brightness": _parse_int(values[13 + shift]),
|
||||
"wifi_connected": _bit_to_bool(values[14 + shift]),
|
||||
"repeat_playlist": _bit_to_bool(values[15 + shift]),
|
||||
"autoplay": AUTOPLAY_MAP.get(values[16 + shift]),
|
||||
"autoclean": _bit_to_bool(values[17 + shift])
|
||||
if len(values) > 17
|
||||
else False,
|
||||
}
|
||||
for key, value in status.items():
|
||||
if (old_value := getattr(self, key, None)) != value:
|
||||
@@ -312,8 +315,10 @@ class OasisMini:
|
||||
raise ValueError("Invalid pause option specified")
|
||||
await self._async_command(params={"WRIWAITAFTER": option})
|
||||
|
||||
async def async_set_playlist(self, playlist: list[int]) -> None:
|
||||
async def async_set_playlist(self, playlist: list[int] | int) -> None:
|
||||
"""Set the playlist."""
|
||||
if isinstance(playlist, int):
|
||||
playlist = [playlist]
|
||||
if is_playing := (self.status_code == 4):
|
||||
await self.async_stop()
|
||||
await self._async_command(params={"WRIJOBLIST": ",".join(map(str, playlist))})
|
||||
|
||||
@@ -246,6 +246,16 @@
|
||||
"1": 14268
|
||||
}
|
||||
},
|
||||
"6260": {
|
||||
"id": 6260,
|
||||
"name": "Atlantic Blue Marlin",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/4b48742bb58d404f8a93e201842387ef.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 35093
|
||||
}
|
||||
},
|
||||
"1996": {
|
||||
"id": 1996,
|
||||
"name": "Axolotl",
|
||||
@@ -256,6 +266,26 @@
|
||||
"1": 6879
|
||||
}
|
||||
},
|
||||
"7404": {
|
||||
"id": 7404,
|
||||
"name": "Axolotle",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/03/f64af5becf1924fc0699119a742a5f9f.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 33808
|
||||
}
|
||||
},
|
||||
"5947": {
|
||||
"id": 5947,
|
||||
"name": "Baby Dino",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2024/12/192338257389d8879217d9daa33745a8.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 28071
|
||||
}
|
||||
},
|
||||
"174": {
|
||||
"id": 174,
|
||||
"name": "Baby Hummingbird",
|
||||
@@ -299,6 +329,16 @@
|
||||
"1": 68353
|
||||
}
|
||||
},
|
||||
"3515": {
|
||||
"id": 3515,
|
||||
"name": "Barracuda",
|
||||
"author": "Luany Camila",
|
||||
"image": "2024/09/604dcb2dbc036c4c49dc94e6c75f5cb9.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 9875
|
||||
}
|
||||
},
|
||||
"194": {
|
||||
"id": 194,
|
||||
"name": "Bass",
|
||||
@@ -363,6 +403,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:47.000000Z"
|
||||
},
|
||||
"6765": {
|
||||
"id": 6765,
|
||||
"name": "Beautiful Geisha",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/3bdcb830fb6909ba5135ceb0f81a510c.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 53742
|
||||
}
|
||||
},
|
||||
"168": {
|
||||
"id": 168,
|
||||
"name": "Betta Fish",
|
||||
@@ -426,6 +476,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:47.000000Z"
|
||||
},
|
||||
"6272": {
|
||||
"id": 6272,
|
||||
"name": "Brontosaurus",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/767fe6711da2605917d112d0bfcd262e.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 75527
|
||||
}
|
||||
},
|
||||
"133": {
|
||||
"id": 133,
|
||||
"name": "Bubbles",
|
||||
@@ -502,6 +562,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-23T15:50:41.000000Z"
|
||||
},
|
||||
"2187": {
|
||||
"id": 2187,
|
||||
"name": "Cachalote",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2024/08/ac6a377297a052639da5f36d206bb557.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 44161
|
||||
}
|
||||
},
|
||||
"58": {
|
||||
"id": 58,
|
||||
"name": "Camalion",
|
||||
@@ -556,6 +626,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:48.000000Z"
|
||||
},
|
||||
"7219": {
|
||||
"id": 7219,
|
||||
"name": "Chinese dragon",
|
||||
"author": "Luany Camila",
|
||||
"image": "2025/02/7499ca933049f6de7abe4fd76221ffb3.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 23348
|
||||
}
|
||||
},
|
||||
"5087": {
|
||||
"id": 5087,
|
||||
"name": "Christmas Angel",
|
||||
@@ -1216,6 +1296,16 @@
|
||||
},
|
||||
"updated_at": "2024-09-05T13:53:22.000000Z"
|
||||
},
|
||||
"7225": {
|
||||
"id": 7225,
|
||||
"name": "Drago jr.",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/62fa0a3bf27a1e7636d53246bfd6abd8.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 41881
|
||||
}
|
||||
},
|
||||
"195": {
|
||||
"id": 195,
|
||||
"name": "Dragon",
|
||||
@@ -1227,6 +1317,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:56.000000Z"
|
||||
},
|
||||
"2118": {
|
||||
"id": 2118,
|
||||
"name": "Drums of Fate",
|
||||
"author": "Anders",
|
||||
"image": "2024/08/1978f6c6169d1f68c163aad94b8b1e95.svg",
|
||||
"clean_pattern": 117,
|
||||
"reduced_svg_content": {
|
||||
"1": 30812
|
||||
}
|
||||
},
|
||||
"193": {
|
||||
"id": 193,
|
||||
"name": "Duck",
|
||||
@@ -1259,6 +1359,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:54.000000Z"
|
||||
},
|
||||
"7255": {
|
||||
"id": 7255,
|
||||
"name": "Elf on a Bike",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/f3a0369934c1f0f1641a87e77129dfa6.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 34869
|
||||
}
|
||||
},
|
||||
"129": {
|
||||
"id": 129,
|
||||
"name": "Engine Turn",
|
||||
@@ -1428,6 +1538,26 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:48.000000Z"
|
||||
},
|
||||
"6447": {
|
||||
"id": 6447,
|
||||
"name": "Flowers and Hearts in love",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/3f0bf99f4c04fa2011bda24ad7b5279f.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 76791
|
||||
}
|
||||
},
|
||||
"759": {
|
||||
"id": 759,
|
||||
"name": "Fox",
|
||||
"author": "Oasis Mini",
|
||||
"image": "2024/07/44c0031863a6d55608483608bb5afdf1.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 7666
|
||||
}
|
||||
},
|
||||
"715": {
|
||||
"id": 715,
|
||||
"name": "Fractospiral",
|
||||
@@ -1579,6 +1709,16 @@
|
||||
"1": 34208
|
||||
}
|
||||
},
|
||||
"7100": {
|
||||
"id": 7100,
|
||||
"name": "Giraffe",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/05c566016b59e48110711e2dd4323a79.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 44743
|
||||
}
|
||||
},
|
||||
"1777": {
|
||||
"id": 1777,
|
||||
"name": "Great Wave",
|
||||
@@ -1754,6 +1894,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T16:01:33.000000Z"
|
||||
},
|
||||
"7154": {
|
||||
"id": 7154,
|
||||
"name": "Heart with Wings",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/677a2ea86594860d7ec17d8cb3eb5352.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 24307
|
||||
}
|
||||
},
|
||||
"356": {
|
||||
"id": 356,
|
||||
"name": "Hedgehog",
|
||||
@@ -1936,6 +2086,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:52.000000Z"
|
||||
},
|
||||
"5637": {
|
||||
"id": 5637,
|
||||
"name": "Jack in the box",
|
||||
"author": "Luany Camila",
|
||||
"image": "2024/12/7d8507f05918c13c709a9c05a72b78c1.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 20697
|
||||
}
|
||||
},
|
||||
"238": {
|
||||
"id": 238,
|
||||
"name": "Jack Russell Terrier",
|
||||
@@ -1979,6 +2139,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:56.000000Z"
|
||||
},
|
||||
"7096": {
|
||||
"id": 7096,
|
||||
"name": "Kangaroo",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/24aed7f4d3b0be562bd2d5e8ec26a774.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 43094
|
||||
}
|
||||
},
|
||||
"2796": {
|
||||
"id": 2796,
|
||||
"name": "Killer Wale",
|
||||
@@ -1989,6 +2159,16 @@
|
||||
"1": 42409
|
||||
}
|
||||
},
|
||||
"7138": {
|
||||
"id": 7138,
|
||||
"name": "Kitty Playing with Yarn",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/03d03c3a6b5ec3df9a3712ecafd1b39d.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 79176
|
||||
}
|
||||
},
|
||||
"239": {
|
||||
"id": 239,
|
||||
"name": "Kobra",
|
||||
@@ -2107,6 +2287,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-23T15:43:06.000000Z"
|
||||
},
|
||||
"6252": {
|
||||
"id": 6252,
|
||||
"name": "Lobster",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/27201ea751b12bbcbfa3062334dbae66.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 44912
|
||||
}
|
||||
},
|
||||
"177": {
|
||||
"id": 177,
|
||||
"name": "Lone Blue Jay Bird",
|
||||
@@ -2213,6 +2403,16 @@
|
||||
"1": 20347
|
||||
}
|
||||
},
|
||||
"7417": {
|
||||
"id": 7417,
|
||||
"name": "Manta Ray",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/03/8a4f9fe0bf4a17ea40013961ba8f1b2d.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 10011
|
||||
}
|
||||
},
|
||||
"339": {
|
||||
"id": 339,
|
||||
"name": "Marmoset Monkey",
|
||||
@@ -2235,6 +2435,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:59:00.000000Z"
|
||||
},
|
||||
"7211": {
|
||||
"id": 7211,
|
||||
"name": "Mindfulness",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/0283bb411e5ff892bf8b5940793aeedd.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 39419
|
||||
}
|
||||
},
|
||||
"78": {
|
||||
"id": 78,
|
||||
"name": "Mini Bouquet",
|
||||
@@ -2288,6 +2498,16 @@
|
||||
"1": 6986
|
||||
}
|
||||
},
|
||||
"7028": {
|
||||
"id": 7028,
|
||||
"name": "Mosasaur",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/e9158184556f47f56a97e5f11e352a0d.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 36811
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"id": 202,
|
||||
"name": "Moth",
|
||||
@@ -2425,6 +2645,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:55.000000Z"
|
||||
},
|
||||
"5994": {
|
||||
"id": 5994,
|
||||
"name": "Old Scholl",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2024/12/55b55eaa86dabe731eab198f1502d63b.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 40716
|
||||
}
|
||||
},
|
||||
"1285": {
|
||||
"id": 1285,
|
||||
"name": "Orbital Spiral",
|
||||
@@ -2622,6 +2852,16 @@
|
||||
"1": 60547
|
||||
}
|
||||
},
|
||||
"7268": {
|
||||
"id": 7268,
|
||||
"name": "Playful Little Robot",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/7ac44b24ff9cff01130cb0cb1309e7f0.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 36853
|
||||
}
|
||||
},
|
||||
"1049": {
|
||||
"id": 1049,
|
||||
"name": "PolarValentine",
|
||||
@@ -2664,6 +2904,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T16:01:31.000000Z"
|
||||
},
|
||||
"6470": {
|
||||
"id": 6470,
|
||||
"name": "Pteranodon",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/990a517bbd8cd99d32c1049751d7028a.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 33688
|
||||
}
|
||||
},
|
||||
"1301": {
|
||||
"id": 1301,
|
||||
"name": "Quantum Entanglement in to out",
|
||||
@@ -2696,6 +2946,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:59.000000Z"
|
||||
},
|
||||
"6000": {
|
||||
"id": 6000,
|
||||
"name": "Rabbit hugging a Heart",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2024/12/0cd7986b975db68f17efbe069c32657a.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 46342
|
||||
}
|
||||
},
|
||||
"1264": {
|
||||
"id": 1264,
|
||||
"name": "Raccoon",
|
||||
@@ -2778,6 +3038,16 @@
|
||||
"1": 33471
|
||||
}
|
||||
},
|
||||
"6778": {
|
||||
"id": 6778,
|
||||
"name": "Robot with a flower",
|
||||
"author": "Luany Camila",
|
||||
"image": "2025/01/a28339205d4c9837241f69064ba48adc.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 36372
|
||||
}
|
||||
},
|
||||
"210": {
|
||||
"id": 210,
|
||||
"name": "Rocket",
|
||||
@@ -2799,6 +3069,16 @@
|
||||
"1": 29953
|
||||
}
|
||||
},
|
||||
"6614": {
|
||||
"id": 6614,
|
||||
"name": "Romantic Valentine Kiss",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/05651f308ce8c114041eb9f46f3b01c4.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 41462
|
||||
}
|
||||
},
|
||||
"105": {
|
||||
"id": 105,
|
||||
"name": "Rooster",
|
||||
@@ -2861,6 +3141,26 @@
|
||||
"1": 15406
|
||||
}
|
||||
},
|
||||
"7304": {
|
||||
"id": 7304,
|
||||
"name": "Sakura Ryu",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/03/1edf5f98df8ea2fec430efbb82392f2d.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 51520
|
||||
}
|
||||
},
|
||||
"6787": {
|
||||
"id": 6787,
|
||||
"name": "Samurai",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/c4722369a149e42d7d8a19db37377380.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 72807
|
||||
}
|
||||
},
|
||||
"4580": {
|
||||
"id": 4580,
|
||||
"name": "Santa Claus",
|
||||
@@ -3104,6 +3404,26 @@
|
||||
"1": 28533
|
||||
}
|
||||
},
|
||||
"5952": {
|
||||
"id": 5952,
|
||||
"name": "Skateasaurus",
|
||||
"author": "Luany Camila",
|
||||
"image": "2024/12/050d6aec400597aa7831ace7909207f1.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 20864
|
||||
}
|
||||
},
|
||||
"7190": {
|
||||
"id": 7190,
|
||||
"name": "Skating Fairy",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/ea3e536c4458d0ca2aab281da553545b.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 44630
|
||||
}
|
||||
},
|
||||
"209": {
|
||||
"id": 209,
|
||||
"name": "Skull",
|
||||
@@ -3169,6 +3489,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:59:33.000000Z"
|
||||
},
|
||||
"7377": {
|
||||
"id": 7377,
|
||||
"name": "Snake Chinese Year",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/03/4a071f3f90ea12025241dd7d1044d651.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 24568
|
||||
}
|
||||
},
|
||||
"551": {
|
||||
"id": 551,
|
||||
"name": "snowflake",
|
||||
@@ -3201,6 +3531,16 @@
|
||||
"1": 4116
|
||||
}
|
||||
},
|
||||
"5564": {
|
||||
"id": 5564,
|
||||
"name": "SnowFlake (C C)",
|
||||
"author": "RiverRatDC",
|
||||
"image": "2024/12/3f5e5f460465ab17ea0003826ac3ded5.svg",
|
||||
"clean_pattern": null,
|
||||
"reduced_svg_content": {
|
||||
"1": 26547
|
||||
}
|
||||
},
|
||||
"4803": {
|
||||
"id": 4803,
|
||||
"name": "Snowman Frosty",
|
||||
@@ -3284,6 +3624,16 @@
|
||||
"1": 6404
|
||||
}
|
||||
},
|
||||
"6609": {
|
||||
"id": 6609,
|
||||
"name": "Spinosaurus",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/aa915de291b74a4a625402d4c6cf0ab7.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 70140
|
||||
}
|
||||
},
|
||||
"1695": {
|
||||
"id": 1695,
|
||||
"name": "spiral",
|
||||
@@ -3697,6 +4047,16 @@
|
||||
"1": 9497
|
||||
}
|
||||
},
|
||||
"6293": {
|
||||
"id": 6293,
|
||||
"name": "Stegosaurus",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/e3e254daf62d1dd69395ed5106c3fd40.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 68903
|
||||
}
|
||||
},
|
||||
"2567": {
|
||||
"id": 2567,
|
||||
"name": "Stellar Sea lion",
|
||||
@@ -3708,6 +4068,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-23T16:00:00.000000Z"
|
||||
},
|
||||
"6739": {
|
||||
"id": 6739,
|
||||
"name": "Stellar Sea Lion over the Rocks",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/df3917677652ff73b194c8a34d773402.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 53595
|
||||
}
|
||||
},
|
||||
"115": {
|
||||
"id": 115,
|
||||
"name": "String ray",
|
||||
@@ -3804,6 +4174,26 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:54.000000Z"
|
||||
},
|
||||
"7276": {
|
||||
"id": 7276,
|
||||
"name": "Takeshi Ren",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/72d29db6dcdef850cc37c2721216989c.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 55111
|
||||
}
|
||||
},
|
||||
"7325": {
|
||||
"id": 7325,
|
||||
"name": "Tango the pango",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/03/42477ad4a8b1d08cb7fc34e9ad4f8314.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 35762
|
||||
}
|
||||
},
|
||||
"5729": {
|
||||
"id": 5729,
|
||||
"name": "The Baby Cat",
|
||||
@@ -3814,6 +4204,36 @@
|
||||
"1": 58681
|
||||
}
|
||||
},
|
||||
"5728": {
|
||||
"id": 5728,
|
||||
"name": "The Baby Cat",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2024/12/2d01ed84e45f5366ff8977e4c18f9df3.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 58681
|
||||
}
|
||||
},
|
||||
"6998": {
|
||||
"id": 6998,
|
||||
"name": "The Camel",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/8ff29f5fa10c0b5213115ae8ebfaa87d.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 61839
|
||||
}
|
||||
},
|
||||
"6992": {
|
||||
"id": 6992,
|
||||
"name": "The Frog",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/8d46682ac8cb96fd1dc7e6c425b5da73.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 56023
|
||||
}
|
||||
},
|
||||
"223": {
|
||||
"id": 223,
|
||||
"name": "The Knot",
|
||||
@@ -4031,6 +4451,16 @@
|
||||
"1": 43134
|
||||
}
|
||||
},
|
||||
"6465": {
|
||||
"id": 6465,
|
||||
"name": "Triceratops",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/236c40e9905d6c835569ba1e123de409.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 51306
|
||||
}
|
||||
},
|
||||
"135": {
|
||||
"id": 135,
|
||||
"name": "Triforce",
|
||||
@@ -4118,6 +4548,16 @@
|
||||
},
|
||||
"updated_at": "2024-08-04T15:58:49.000000Z"
|
||||
},
|
||||
"2207": {
|
||||
"id": 2207,
|
||||
"name": "UConn",
|
||||
"author": "JeffDLowe",
|
||||
"image": "2024/08/841e0c9b75cbe33f152672f76d8909b6.svg",
|
||||
"clean_pattern": null,
|
||||
"reduced_svg_content": {
|
||||
"1": 23086
|
||||
}
|
||||
},
|
||||
"218": {
|
||||
"id": 218,
|
||||
"name": "Unicorn",
|
||||
@@ -4139,6 +4579,46 @@
|
||||
"1": 21080
|
||||
}
|
||||
},
|
||||
"6472": {
|
||||
"id": 6472,
|
||||
"name": "Valentine Cupid",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/89da4980fb95168472db914362d73fc0.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 60236
|
||||
}
|
||||
},
|
||||
"6810": {
|
||||
"id": 6810,
|
||||
"name": "Valentine’s Heart-Shaped Box",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/ecb41379a6cf281dbed0b95d4e4b8126.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 63975
|
||||
}
|
||||
},
|
||||
"7029": {
|
||||
"id": 7029,
|
||||
"name": "Valentine’s Moon",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/96d1d52db7f234a78913e53a003129cd.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 39111
|
||||
}
|
||||
},
|
||||
"6594": {
|
||||
"id": 6594,
|
||||
"name": "Velociraptor",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/01/53a5ff36f6b4b06df954601622c6b1a3.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 49603
|
||||
}
|
||||
},
|
||||
"2116": {
|
||||
"id": 2116,
|
||||
"name": "Void of Death",
|
||||
@@ -4242,6 +4722,16 @@
|
||||
"1": 2310
|
||||
}
|
||||
},
|
||||
"2146": {
|
||||
"id": 2146,
|
||||
"name": "Warped Waves",
|
||||
"author": "Crystal James",
|
||||
"image": "2024/08/794b3a39e45f75fd487728e5cf9fb1ed.svg",
|
||||
"clean_pattern": null,
|
||||
"reduced_svg_content": {
|
||||
"1": 17243
|
||||
}
|
||||
},
|
||||
"555": {
|
||||
"id": 555,
|
||||
"name": "Wavy dude",
|
||||
@@ -4356,6 +4846,16 @@
|
||||
"1": 42218
|
||||
}
|
||||
},
|
||||
"7221": {
|
||||
"id": 7221,
|
||||
"name": "Winter Sand",
|
||||
"author": "000843.bf34f133184943939b950674aaaa4ac9.2327",
|
||||
"image": "2025/02/48f5d7a080145e4f8442eb0b1b5f3872.svg",
|
||||
"clean_pattern": null,
|
||||
"reduced_svg_content": {
|
||||
"1": 17842
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"id": 500,
|
||||
"name": "Wipe Left to Right",
|
||||
@@ -4493,6 +4993,16 @@
|
||||
"1": 37914
|
||||
}
|
||||
},
|
||||
"7233": {
|
||||
"id": 7233,
|
||||
"name": "Yoga Tree Pose",
|
||||
"author": "Otávio Bittencourt",
|
||||
"image": "2025/02/cc38fea088324f283faaaebe1feef4d9.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 51214
|
||||
}
|
||||
},
|
||||
"437": {
|
||||
"id": 437,
|
||||
"name": "Yorkshire",
|
||||
@@ -4503,5 +5013,15 @@
|
||||
"1": 23212
|
||||
},
|
||||
"updated_at": "2024-08-04T16:01:31.000000Z"
|
||||
},
|
||||
"4014": {
|
||||
"id": 4014,
|
||||
"name": "Zombie",
|
||||
"author": "Luany Camila",
|
||||
"image": "2024/09/3a42cbf8da47cf3a922fab6246093d2d.svg",
|
||||
"clean_pattern": 119,
|
||||
"reduced_svg_content": {
|
||||
"1": 21411
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,14 @@ def _bit_to_bool(val: str) -> bool:
|
||||
return val == "1"
|
||||
|
||||
|
||||
def _parse_int(val: str) -> int:
|
||||
"""Convert an int string to int."""
|
||||
try:
|
||||
return int(val)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def draw_svg(track: dict, progress: int, model_id: str) -> str | None:
|
||||
"""Draw SVG."""
|
||||
if track and (svg_content := track.get("svg_content")):
|
||||
|
||||
@@ -85,20 +85,20 @@ def playlist_update_handler(entity: OasisMiniSelectEntity) -> None:
|
||||
|
||||
|
||||
DESCRIPTORS = (
|
||||
OasisMiniSelectEntityDescription(
|
||||
key="playlist",
|
||||
name="Playlist",
|
||||
current_value=lambda device: (device.playlist.copy(), device.playlist_index),
|
||||
select_fn=lambda device, option: device.async_change_track(option),
|
||||
update_handler=playlist_update_handler,
|
||||
),
|
||||
OasisMiniSelectEntityDescription(
|
||||
key="autoplay",
|
||||
name="Autoplay",
|
||||
translation_key="autoplay",
|
||||
options=list(AUTOPLAY_MAP.values()),
|
||||
current_value=lambda device: device.autoplay,
|
||||
select_fn=lambda device, option: device.async_set_autoplay(option),
|
||||
),
|
||||
OasisMiniSelectEntityDescription(
|
||||
key="playlist",
|
||||
translation_key="playlist",
|
||||
current_value=lambda device: (device.playlist.copy(), device.playlist_index),
|
||||
select_fn=lambda device, option: device.async_change_track(option),
|
||||
update_handler=playlist_update_handler,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -39,34 +39,27 @@ async def async_setup_entry(
|
||||
DESCRIPTORS = {
|
||||
SensorEntityDescription(
|
||||
key="download_progress",
|
||||
translation_key="download_progress",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
name="Download progress",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
} | {
|
||||
SensorEntityDescription(
|
||||
key=key,
|
||||
name=key.replace("_", " ").capitalize(),
|
||||
translation_key=key,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
)
|
||||
for key in (
|
||||
"busy",
|
||||
"error",
|
||||
"led_color_id",
|
||||
"status",
|
||||
"wifi_connected",
|
||||
)
|
||||
for key in ("error", "led_color_id", "status")
|
||||
}
|
||||
|
||||
CLOUD_DESCRIPTORS = (
|
||||
SensorEntityDescription(
|
||||
key="drawing_progress",
|
||||
translation_key="drawing_progress",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
name="Drawing progress",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
|
||||
@@ -38,7 +38,53 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"random_track": {
|
||||
"name": "Play random track"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"busy": {
|
||||
"name": "Busy"
|
||||
},
|
||||
"wifi_status": {
|
||||
"name": "Wi-Fi status"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"led": {
|
||||
"name": "LED"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"ball_speed": {
|
||||
"name": "Ball speed"
|
||||
},
|
||||
"led_speed": {
|
||||
"name": "LED speed"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"autoplay": {
|
||||
"name": "Autoplay"
|
||||
},
|
||||
"playlist": {
|
||||
"name": "Playlist"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"download_progress": {
|
||||
"name": "Download progress"
|
||||
},
|
||||
"drawing_progress": {
|
||||
"name": "Drawing progress"
|
||||
},
|
||||
"error": {
|
||||
"name": "Error"
|
||||
},
|
||||
"led_color_id": {
|
||||
"name": "LED color ID"
|
||||
},
|
||||
"status": {
|
||||
"name": "Status",
|
||||
"state": {
|
||||
@@ -54,5 +100,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_busy": {
|
||||
"message": "{name} is currently busy and cannot be modified"
|
||||
},
|
||||
"invalid_media": {
|
||||
"message": "Invalid media: {media}"
|
||||
},
|
||||
"playlists_unsupported": {
|
||||
"message": "Playlists are not currently supported"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,53 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"random_track": {
|
||||
"name": "Play random track"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"busy": {
|
||||
"name": "Busy"
|
||||
},
|
||||
"wifi_status": {
|
||||
"name": "Wi-Fi status"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"led": {
|
||||
"name": "LED"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"ball_speed": {
|
||||
"name": "Ball speed"
|
||||
},
|
||||
"led_speed": {
|
||||
"name": "LED speed"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"autoplay": {
|
||||
"name": "Autoplay"
|
||||
},
|
||||
"playlist": {
|
||||
"name": "Playlist"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"download_progress": {
|
||||
"name": "Download progress"
|
||||
},
|
||||
"drawing_progress": {
|
||||
"name": "Drawing progress"
|
||||
},
|
||||
"error": {
|
||||
"name": "Error"
|
||||
},
|
||||
"led_color_id": {
|
||||
"name": "LED color ID"
|
||||
},
|
||||
"status": {
|
||||
"name": "Status",
|
||||
"state": {
|
||||
@@ -54,5 +100,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_busy": {
|
||||
"message": "{name} is currently busy and cannot be modified"
|
||||
},
|
||||
"invalid_media": {
|
||||
"message": "Invalid media: {media}"
|
||||
},
|
||||
"playlists_unsupported": {
|
||||
"message": "Playlists are not currently supported"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Oasis Mini",
|
||||
"homeassistant": "2024.4.0",
|
||||
"homeassistant": "2024.5.0",
|
||||
"render_readme": true,
|
||||
"zip_release": true,
|
||||
"filename": "oasis_mini.zip"
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Home Assistant
|
||||
homeassistant>=2024.4
|
||||
homeassistant>=2025.1
|
||||
numpy
|
||||
PyTurboJPEG
|
||||
|
||||
# Integration
|
||||
aiohttp
|
||||
aiohttp # should already be installed with Home Assistant
|
||||
cryptography # should already be installed with Home Assistant
|
||||
|
||||
# Development
|
||||
colorlog
|
||||
pip>=21.0
|
||||
pre-commit
|
||||
ruff
|
||||
@@ -1,9 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
sudo apt-get update && sudo apt-get install libturbojpeg0 libpcap0.8 -y
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
python3 -m pip install --requirement requirements.txt --upgrade
|
||||
|
||||
pre-commit install
|
||||
|
||||
mkdir -p config
|
||||
Reference in New Issue
Block a user