inky-image-display-shared 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inky_image_display_shared-0.2.0/PKG-INFO +10 -0
- inky_image_display_shared-0.2.0/pyproject.toml +17 -0
- inky_image_display_shared-0.2.0/src/inky_image_display_shared/__init__.py +1 -0
- inky_image_display_shared-0.2.0/src/inky_image_display_shared/models/__init__.py +13 -0
- inky_image_display_shared-0.2.0/src/inky_image_display_shared/models/device.py +71 -0
- inky_image_display_shared-0.2.0/src/inky_image_display_shared/models/image.py +64 -0
- inky_image_display_shared-0.2.0/src/inky_image_display_shared/models/immich_sync_job.py +117 -0
- inky_image_display_shared-0.2.0/src/inky_image_display_shared/schemas/__init__.py +17 -0
- inky_image_display_shared-0.2.0/src/inky_image_display_shared/schemas/commands.py +59 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inky-image-display-shared
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Shared models and schemas for Inky Image Display services.
|
|
5
|
+
Author: stkr22
|
|
6
|
+
Author-email: stkr22 <stkr22@github.com>
|
|
7
|
+
License-Expression: GPL-3.0-only
|
|
8
|
+
Requires-Dist: pydantic~=2.12.5
|
|
9
|
+
Requires-Dist: sqlmodel~=0.0.38
|
|
10
|
+
Requires-Python: >=3.12, <3.14
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11.6,<0.12"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "inky-image-display-shared"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Shared models and schemas for Inky Image Display services."
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "stkr22", email = "stkr22@github.com" }
|
|
11
|
+
]
|
|
12
|
+
license = "GPL-3.0-only"
|
|
13
|
+
requires-python = ">=3.12,<3.14"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"pydantic~=2.12.5",
|
|
16
|
+
"sqlmodel~=0.0.38",
|
|
17
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared models and schemas for Inky Image Display services."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""SQLModel database models for Inky Image Display."""
|
|
2
|
+
|
|
3
|
+
from .device import Device, DeviceDisplayState
|
|
4
|
+
from .image import Image
|
|
5
|
+
from .immich_sync_job import ImmichSyncJob, SyncStrategy
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Device",
|
|
9
|
+
"DeviceDisplayState",
|
|
10
|
+
"Image",
|
|
11
|
+
"ImmichSyncJob",
|
|
12
|
+
"SyncStrategy",
|
|
13
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Device display state model for Inky displays."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from uuid import UUID, uuid4
|
|
5
|
+
|
|
6
|
+
from sqlmodel import Field, SQLModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DeviceDisplayState(SQLModel, table=True):
|
|
10
|
+
"""Legacy display state for a device.
|
|
11
|
+
|
|
12
|
+
Used by the skill package. New services should use the ``Device`` model
|
|
13
|
+
instead.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
global_device_id: Primary key (UUID, previously FK to global_devices)
|
|
17
|
+
is_online: Whether device is currently reachable
|
|
18
|
+
current_image_id: Currently displayed image (nullable)
|
|
19
|
+
displayed_since: When current image was displayed
|
|
20
|
+
scheduled_next_at: When to show next image
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__tablename__ = "device_display_states"
|
|
25
|
+
|
|
26
|
+
global_device_id: UUID = Field(primary_key=True)
|
|
27
|
+
is_online: bool = Field(default=True, description="Whether device is currently reachable")
|
|
28
|
+
current_image_id: UUID | None = Field(default=None, foreign_key="images.id")
|
|
29
|
+
displayed_since: datetime | None = Field(default=None, description="When current image was displayed")
|
|
30
|
+
scheduled_next_at: datetime = Field(default_factory=datetime.now, description="Scheduled time for next image")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Device(SQLModel, table=True):
|
|
34
|
+
"""API-managed device record.
|
|
35
|
+
|
|
36
|
+
Replaces the combination of GlobalDevice + DeviceDisplayState for the API
|
|
37
|
+
service. The skill continues to use DeviceDisplayState + GlobalDevice
|
|
38
|
+
unchanged.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
id: Unique identifier
|
|
42
|
+
device_id: String device identifier (e.g. 'inky-kitchen')
|
|
43
|
+
room: Room where device is located
|
|
44
|
+
display_width: Display width in pixels
|
|
45
|
+
display_height: Display height in pixels
|
|
46
|
+
display_orientation: Display orientation
|
|
47
|
+
display_model: Display hardware model
|
|
48
|
+
is_online: Whether device is currently connected
|
|
49
|
+
current_image_id: Currently displayed image
|
|
50
|
+
displayed_since: When current image was displayed
|
|
51
|
+
scheduled_next_at: When to show next image
|
|
52
|
+
created_at: When record was created
|
|
53
|
+
updated_at: When record was last updated
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
__tablename__ = "devices"
|
|
58
|
+
|
|
59
|
+
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
|
60
|
+
device_id: str = Field(unique=True, index=True, description="Device string identifier, e.g. 'inky-kitchen'")
|
|
61
|
+
room: str | None = Field(default=None)
|
|
62
|
+
display_width: int = Field(default=1600)
|
|
63
|
+
display_height: int = Field(default=1200)
|
|
64
|
+
display_orientation: str = Field(default="landscape")
|
|
65
|
+
display_model: str = Field(default="inky_impression_13_spectra6")
|
|
66
|
+
is_online: bool = Field(default=False)
|
|
67
|
+
current_image_id: UUID | None = Field(default=None, foreign_key="images.id")
|
|
68
|
+
displayed_since: datetime | None = Field(default=None)
|
|
69
|
+
scheduled_next_at: datetime = Field(default_factory=datetime.now)
|
|
70
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
71
|
+
updated_at: datetime = Field(default_factory=datetime.now)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Image model for storing picture metadata."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from uuid import UUID, uuid4
|
|
5
|
+
|
|
6
|
+
from sqlmodel import Field, SQLModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Image(SQLModel, table=True):
|
|
10
|
+
"""Image metadata stored in the database.
|
|
11
|
+
|
|
12
|
+
Images are fetched from various sources (manual upload, Immich, Unsplash, etc.)
|
|
13
|
+
and stored in MinIO. This table tracks metadata for display scheduling and
|
|
14
|
+
voice command responses.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
id: Unique identifier for the image
|
|
18
|
+
source_name: Source identifier (e.g., "manual", "immich", "unsplash")
|
|
19
|
+
storage_path: Path to image in MinIO bucket
|
|
20
|
+
title: Optional title for voice responses
|
|
21
|
+
description: Optional description for "what am I seeing?" queries
|
|
22
|
+
author: Optional author/photographer name
|
|
23
|
+
source_url: Optional URL to original source
|
|
24
|
+
display_duration_seconds: How long to show this image (default 3600)
|
|
25
|
+
priority: Weight for future priority-based selection (default 0)
|
|
26
|
+
original_width: Image width in pixels
|
|
27
|
+
original_height: Image height in pixels
|
|
28
|
+
last_displayed_at: When the image was last shown (for FIFO selection)
|
|
29
|
+
expires_at: Optional expiration time for auto-cleanup
|
|
30
|
+
created_at: When record was created (replaces fetched_at)
|
|
31
|
+
updated_at: When record was last updated
|
|
32
|
+
tags: Comma-separated tags for categorization
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
__tablename__ = "images"
|
|
37
|
+
|
|
38
|
+
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
|
39
|
+
source_name: str = Field(index=True, description="Source identifier (e.g. manual, unsplash)")
|
|
40
|
+
storage_path: str = Field(description="MinIO object path")
|
|
41
|
+
|
|
42
|
+
# Metadata for voice responses
|
|
43
|
+
title: str | None = Field(default=None, description="Image title for voice responses")
|
|
44
|
+
description: str | None = Field(default=None, description="Description for 'what am I seeing?'")
|
|
45
|
+
author: str | None = Field(default=None, description="Author/photographer name")
|
|
46
|
+
source_url: str | None = Field(default=None, description="Original source URL")
|
|
47
|
+
|
|
48
|
+
# Display settings
|
|
49
|
+
display_duration_seconds: int = Field(default=600, description="Display duration in seconds")
|
|
50
|
+
priority: int = Field(default=5, description="Priority weight for selection (higher = more likely)")
|
|
51
|
+
|
|
52
|
+
# Image dimensions and orientation for device compatibility
|
|
53
|
+
original_width: int | None = Field(default=None, description="Image width in pixels")
|
|
54
|
+
original_height: int | None = Field(default=None, description="Image height in pixels")
|
|
55
|
+
is_portrait: bool = Field(default=False, description="True if image is portrait-oriented (height > width)")
|
|
56
|
+
|
|
57
|
+
# Timestamps
|
|
58
|
+
last_displayed_at: datetime | None = Field(default=None, description="Last display time for FIFO")
|
|
59
|
+
expires_at: datetime | None = Field(default=None, description="Expiration time for cleanup")
|
|
60
|
+
created_at: datetime = Field(default_factory=datetime.now, description="When record was created")
|
|
61
|
+
updated_at: datetime = Field(default_factory=datetime.now, description="When record was last updated")
|
|
62
|
+
|
|
63
|
+
# Categorization
|
|
64
|
+
tags: str | None = Field(default=None, description="Comma-separated tags for categorization")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Immich sync job configuration model."""
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from uuid import UUID, uuid4
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import Column
|
|
8
|
+
from sqlalchemy.types import JSON
|
|
9
|
+
from sqlmodel import Field, SQLModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SyncStrategy(enum.StrEnum):
|
|
13
|
+
"""Strategy for selecting images from Immich.
|
|
14
|
+
|
|
15
|
+
Values are uppercase to match PostgreSQL enum labels created by SQLAlchemy.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
RANDOM = "RANDOM"
|
|
19
|
+
SMART = "SMART" # CLIP semantic search
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ImmichSyncJob(SQLModel, table=True):
|
|
23
|
+
"""Immich sync job configuration stored in database.
|
|
24
|
+
|
|
25
|
+
Each job defines a set of filters and selection criteria for syncing
|
|
26
|
+
images from Immich. Jobs can be activated/deactivated and target
|
|
27
|
+
specific devices to determine display requirements.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
id: Unique job identifier
|
|
31
|
+
name: Human-readable job name (unique)
|
|
32
|
+
is_active: Whether job should be executed during sync
|
|
33
|
+
target_device_id: Device this job syncs for (determines dimensions/orientation)
|
|
34
|
+
strategy: Selection strategy - 'random' or 'smart' (CLIP search)
|
|
35
|
+
query: Semantic search query (required for 'smart' strategy)
|
|
36
|
+
count: Number of images to sync per run
|
|
37
|
+
random_pick: Randomly sample from smart search results
|
|
38
|
+
overfetch_multiplier: Fetch more images to allow for client-side filtering
|
|
39
|
+
album_ids: Filter by album UUIDs
|
|
40
|
+
person_ids: Filter by recognized person UUIDs
|
|
41
|
+
tag_ids: Filter by tag UUIDs
|
|
42
|
+
is_favorite: Filter favorites only
|
|
43
|
+
city: Filter by city name
|
|
44
|
+
state: Filter by state/region
|
|
45
|
+
country: Filter by country
|
|
46
|
+
taken_after: Photos taken after this date
|
|
47
|
+
taken_before: Photos taken before this date
|
|
48
|
+
make: Camera make filter
|
|
49
|
+
camera_model: Camera model filter
|
|
50
|
+
rating: Minimum rating filter (0-5)
|
|
51
|
+
min_color_score: Minimum color compatibility for Spectra 6 (0.0-1.0)
|
|
52
|
+
min_vibrancy_score: Minimum vibrancy (saturation/contrast) for e-ink (0.0-1.0)
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
__tablename__ = "immich_sync_jobs"
|
|
57
|
+
|
|
58
|
+
id: UUID = Field(default_factory=uuid4, primary_key=True)
|
|
59
|
+
name: str = Field(unique=True, index=True, description="Unique job name")
|
|
60
|
+
is_active: bool = Field(default=True, description="Whether job is active")
|
|
61
|
+
|
|
62
|
+
# Target device - determines display requirements (width, height, orientation)
|
|
63
|
+
target_device_id: UUID = Field(
|
|
64
|
+
foreign_key="devices.id",
|
|
65
|
+
description="Device this job syncs for. Display dimensions from the device record.",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Selection strategy
|
|
69
|
+
strategy: SyncStrategy = Field(
|
|
70
|
+
default=SyncStrategy.RANDOM,
|
|
71
|
+
description="Selection strategy: 'random' or 'smart' (CLIP semantic search)",
|
|
72
|
+
)
|
|
73
|
+
query: str | None = Field(
|
|
74
|
+
default=None,
|
|
75
|
+
description="Semantic search query for 'smart' strategy",
|
|
76
|
+
)
|
|
77
|
+
count: int = Field(default=10, ge=1, le=1000, description="Images to sync per run")
|
|
78
|
+
random_pick: bool = Field(
|
|
79
|
+
default=False,
|
|
80
|
+
description="Randomly sample from smart search results",
|
|
81
|
+
)
|
|
82
|
+
overfetch_multiplier: int = Field(
|
|
83
|
+
default=3,
|
|
84
|
+
ge=1,
|
|
85
|
+
le=10,
|
|
86
|
+
description="Multiplier for overfetching when client-side filters active",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# API filters (stored as JSON for list types)
|
|
90
|
+
album_ids: list[str] | None = Field(default=None, sa_column=Column(JSON))
|
|
91
|
+
person_ids: list[str] | None = Field(default=None, sa_column=Column(JSON))
|
|
92
|
+
tag_ids: list[str] | None = Field(default=None, sa_column=Column(JSON))
|
|
93
|
+
is_favorite: bool | None = Field(default=None, description="Filter favorites only")
|
|
94
|
+
city: str | None = Field(default=None, description="Filter by city")
|
|
95
|
+
state: str | None = Field(default=None, description="Filter by state/region")
|
|
96
|
+
country: str | None = Field(default=None, description="Filter by country")
|
|
97
|
+
taken_after: datetime | None = Field(default=None, description="Photos taken after")
|
|
98
|
+
taken_before: datetime | None = Field(default=None, description="Photos taken before")
|
|
99
|
+
rating: int | None = Field(default=None, ge=0, le=5, description="Minimum rating")
|
|
100
|
+
|
|
101
|
+
# Client-side filters
|
|
102
|
+
min_color_score: float = Field(
|
|
103
|
+
default=0.5,
|
|
104
|
+
ge=0.0,
|
|
105
|
+
le=1.0,
|
|
106
|
+
description="Minimum color compatibility score for Spectra 6 palette",
|
|
107
|
+
)
|
|
108
|
+
min_vibrancy_score: float = Field(
|
|
109
|
+
default=0.2,
|
|
110
|
+
ge=0.0,
|
|
111
|
+
le=1.0,
|
|
112
|
+
description="Minimum vibrancy score (saturation or contrast) for e-ink suitability",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Timestamps
|
|
116
|
+
created_at: datetime = Field(default_factory=datetime.now, description="When created")
|
|
117
|
+
updated_at: datetime = Field(default_factory=datetime.now, description="When last updated")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Pydantic schemas for inter-service communication."""
|
|
2
|
+
|
|
3
|
+
from .commands import (
|
|
4
|
+
DeviceAcknowledge,
|
|
5
|
+
DeviceRegistration,
|
|
6
|
+
DisplayCommand,
|
|
7
|
+
DisplayInfo,
|
|
8
|
+
RegistrationResponse,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DeviceAcknowledge",
|
|
13
|
+
"DeviceRegistration",
|
|
14
|
+
"DisplayCommand",
|
|
15
|
+
"DisplayInfo",
|
|
16
|
+
"RegistrationResponse",
|
|
17
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Command and response schemas for device communication.
|
|
2
|
+
|
|
3
|
+
These models define the contract between skill/API and device controllers.
|
|
4
|
+
Used for both MQTT and WebSocket communication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DisplayInfo(BaseModel):
|
|
13
|
+
"""Display hardware characteristics sent during device registration."""
|
|
14
|
+
|
|
15
|
+
width: int = Field(default=1600, description="Display width in pixels")
|
|
16
|
+
height: int = Field(default=1200, description="Display height in pixels")
|
|
17
|
+
orientation: Literal["landscape", "portrait"] = Field(default="landscape", description="Display orientation")
|
|
18
|
+
model: str = Field(default="inky_impression_13_spectra6", description="Display model identifier")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeviceRegistration(BaseModel):
|
|
22
|
+
"""Device registration payload.
|
|
23
|
+
|
|
24
|
+
Sent by the device on startup to announce itself.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
device_id: str = Field(description="Unique device identifier")
|
|
28
|
+
display: DisplayInfo = Field(default_factory=DisplayInfo, description="Display hardware characteristics")
|
|
29
|
+
room: str | None = Field(default=None, description="Room where device is located")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RegistrationResponse(BaseModel):
|
|
33
|
+
"""Response sent after successful device registration."""
|
|
34
|
+
|
|
35
|
+
status: Literal["registered", "updated"] = Field(description="Registration result")
|
|
36
|
+
s3_endpoint: str = Field(description="S3 server endpoint")
|
|
37
|
+
s3_bucket: str = Field(description="Bucket containing images")
|
|
38
|
+
s3_access_key: str = Field(description="Read-only access key")
|
|
39
|
+
s3_secret_key: str = Field(description="Read-only secret key")
|
|
40
|
+
s3_secure: bool = Field(default=False, description="Use HTTPS for S3")
|
|
41
|
+
s3_region: str | None = Field(default=None, description="S3 region")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DisplayCommand(BaseModel):
|
|
45
|
+
"""Command sent to device to control the display."""
|
|
46
|
+
|
|
47
|
+
action: Literal["display", "clear", "status"] = Field(description="Command action")
|
|
48
|
+
image_path: str | None = Field(default=None, description="S3 object path for display action")
|
|
49
|
+
image_id: str | None = Field(default=None, description="Image UUID for tracking")
|
|
50
|
+
title: str | None = Field(default=None, description="Image title for device logging")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DeviceAcknowledge(BaseModel):
|
|
54
|
+
"""Acknowledgment sent by device after processing a command."""
|
|
55
|
+
|
|
56
|
+
device_id: str = Field(description="Device identifier")
|
|
57
|
+
image_id: str | None = Field(default=None, description="Currently displayed image UUID")
|
|
58
|
+
successful_display_change: bool = Field(description="Whether the display change was successful")
|
|
59
|
+
error: str | None = Field(default=None, description="Error message if any")
|