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.
@@ -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")