ingest command working
This commit is contained in:
+102
-92
@@ -11,198 +11,178 @@
|
||||
"dirs": [
|
||||
{
|
||||
"name": "A-NEW-DAWN---TRL--23-06-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898002",
|
||||
"unique": "1788972262.89801"
|
||||
"modified_time": "2026-09-10T14:17:01.260076",
|
||||
"unique": "1789042621.26022"
|
||||
},
|
||||
{
|
||||
"name": "BEAST---TRL-90----31-07-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898043",
|
||||
"unique": "1788972262.898044"
|
||||
"modified_time": "2026-09-10T14:17:01.260241",
|
||||
"unique": "1789042621.260242"
|
||||
},
|
||||
{
|
||||
"name": "CINEMA-IN-FESTA-15--18-GIUGNO-2026--CIN-REV26_TLR-15---SPOT-15---ANIMATO--09-06-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898054",
|
||||
"unique": "1788972262.898055"
|
||||
},
|
||||
{
|
||||
"name": "COYOTE-VS-ACME---TRL-60---NEW--06-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898064",
|
||||
"unique": "1788972262.898065"
|
||||
},
|
||||
{
|
||||
"name": "COYOTE-VS-ACME---TRL-90----21-07-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898074",
|
||||
"unique": "1788972262.898074"
|
||||
},
|
||||
{
|
||||
"name": "COYOTE-VS-ACME--COYOTEVSACME30----TRL-30----22-06-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898083",
|
||||
"unique": "1788972262.898084"
|
||||
"modified_time": "2026-09-10T14:17:01.260246",
|
||||
"unique": "1789042621.260247"
|
||||
},
|
||||
{
|
||||
"name": "ERA-MEGLIO-DOMANI---TRL--30-06-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898093",
|
||||
"unique": "1788972262.898093"
|
||||
"modified_time": "2026-09-10T14:17:01.260249",
|
||||
"unique": "1789042621.26025"
|
||||
},
|
||||
{
|
||||
"name": "FERRARI-250-GTO---TRL--06-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898102",
|
||||
"unique": "1788972262.898102"
|
||||
"modified_time": "2026-09-10T14:17:01.260252",
|
||||
"unique": "1789042621.260253"
|
||||
},
|
||||
{
|
||||
"name": "FRAMMENTI---TRL-90----13-05-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898111",
|
||||
"unique": "1788972262.898112"
|
||||
"modified_time": "2026-09-10T14:17:01.260256",
|
||||
"unique": "1789042621.260256"
|
||||
},
|
||||
{
|
||||
"name": "GODZILLA-MINUS-ZERO---TRL-83---04-09-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898121",
|
||||
"unique": "1788972262.898121"
|
||||
"modified_time": "2026-09-10T14:17:01.260259",
|
||||
"unique": "1789042621.260259"
|
||||
},
|
||||
{
|
||||
"name": "HOKUM--HOKUMFADO----TSR-40----11-05-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898130",
|
||||
"unique": "1788972262.898131"
|
||||
"modified_time": "2026-09-10T14:17:01.260262",
|
||||
"unique": "1789042621.260262"
|
||||
},
|
||||
{
|
||||
"name": "HOKUM--HOKUM_BELLS----TSR-30----11-05-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898139",
|
||||
"unique": "1788972262.89814"
|
||||
"modified_time": "2026-09-10T14:17:01.260264",
|
||||
"unique": "1789042621.260265"
|
||||
},
|
||||
{
|
||||
"name": "HOPPER-E-IL-SEGRETO-DELLA-GRANDE-MARMOTTA---TRL-90----13-02-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898149",
|
||||
"unique": "1788972262.898149"
|
||||
"modified_time": "2026-09-10T14:17:01.260267",
|
||||
"unique": "1789042621.260268"
|
||||
},
|
||||
{
|
||||
"name": "HUNGER-GAMES-6--HGALBAMIETITU----TLR-150--30-04-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898158",
|
||||
"unique": "1788972262.898158"
|
||||
"modified_time": "2026-09-10T14:17:01.260270",
|
||||
"unique": "1789042621.260271"
|
||||
},
|
||||
{
|
||||
"name": "HUNGER-GAMES-6--HGALBAMIETITU----TLR-90----05-05-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898167",
|
||||
"unique": "1788972262.898168"
|
||||
"modified_time": "2026-09-10T14:17:01.260290",
|
||||
"unique": "1789042621.260291"
|
||||
},
|
||||
{
|
||||
"name": "HUNGER-GAMES-6--HGALBAMIETITU----TLR-D-60----11-05-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898177",
|
||||
"unique": "1788972262.898177"
|
||||
"modified_time": "2026-09-10T14:17:01.260293",
|
||||
"unique": "1789042621.260294"
|
||||
},
|
||||
{
|
||||
"name": "I-FIGLI-DELLA-SCIMMIA---TRL-1-120----06-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898186",
|
||||
"unique": "1788972262.898186"
|
||||
"modified_time": "2026-09-10T14:17:01.260296",
|
||||
"unique": "1789042621.260297"
|
||||
},
|
||||
{
|
||||
"name": "I-FIGLI-DELLA-SCIMMIA---TRL-1-90----06-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898195",
|
||||
"unique": "1788972262.898196"
|
||||
"modified_time": "2026-09-10T14:17:01.260299",
|
||||
"unique": "1789042621.2603"
|
||||
},
|
||||
{
|
||||
"name": "ICE-CREAM-MAN--ICM90----TRL-90----29-07-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898205",
|
||||
"unique": "1788972262.898205"
|
||||
"modified_time": "2026-09-10T14:17:01.260302",
|
||||
"unique": "1789042621.260303"
|
||||
},
|
||||
{
|
||||
"name": "IO-NON-TI-LASCIO-SOLO---TRL--28-04-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898214",
|
||||
"unique": "1788972262.898214"
|
||||
"modified_time": "2026-09-10T14:17:01.260305",
|
||||
"unique": "1789042621.260305"
|
||||
},
|
||||
{
|
||||
"name": "L-ALBERO-DI-MOLTO-LONTANO---TRL90----14-07-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898223",
|
||||
"unique": "1788972262.898224"
|
||||
"modified_time": "2026-09-10T14:17:01.260308",
|
||||
"unique": "1789042621.260308"
|
||||
},
|
||||
{
|
||||
"name": "LE-AQUILE-DELLA-REPUBBLICA---TLR-B_CON_BOLLINI--08-01-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898232",
|
||||
"unique": "1788972262.898233"
|
||||
"modified_time": "2026-09-10T14:17:01.260310",
|
||||
"unique": "1789042621.260311"
|
||||
},
|
||||
{
|
||||
"name": "MAIGRET-L-AMORE-E-LA-MORTE---TRL--27-07-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898241",
|
||||
"unique": "1788972262.898242"
|
||||
"modified_time": "2026-09-10T14:17:01.260313",
|
||||
"unique": "1789042621.260313"
|
||||
},
|
||||
{
|
||||
"name": "ONSLAUGHT-ASSALTO-FINALE---TRL-1--10-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898262",
|
||||
"unique": "1788972262.898262"
|
||||
"modified_time": "2026-09-10T14:17:01.260315",
|
||||
"unique": "1789042621.260316"
|
||||
},
|
||||
{
|
||||
"name": "QUASI-VERA---TRL-1-60----30-07-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898271",
|
||||
"unique": "1788972262.898272"
|
||||
"modified_time": "2026-09-10T14:17:01.260319",
|
||||
"unique": "1789042621.260319"
|
||||
},
|
||||
{
|
||||
"name": "QUASI-VERA---TRL-1-90----28-07-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898281",
|
||||
"unique": "1788972262.898281"
|
||||
"modified_time": "2026-09-10T14:17:01.260321",
|
||||
"unique": "1789042621.260322"
|
||||
},
|
||||
{
|
||||
"name": "RIDER---TRL-1--13-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898290",
|
||||
"unique": "1788972262.898291"
|
||||
"modified_time": "2026-09-10T14:17:01.260324",
|
||||
"unique": "1789042621.260324"
|
||||
},
|
||||
{
|
||||
"name": "RITORNO-A-BUENOS-AIRES---TRL-145----03-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898300",
|
||||
"unique": "1788972262.8983"
|
||||
"modified_time": "2026-09-10T14:17:01.260327",
|
||||
"unique": "1789042621.260328"
|
||||
},
|
||||
{
|
||||
"name": "RITORNO-A-BUENOS-AIRES---TRL-60----03-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898309",
|
||||
"unique": "1788972262.898309"
|
||||
"modified_time": "2026-09-10T14:17:01.260330",
|
||||
"unique": "1789042621.260331"
|
||||
},
|
||||
{
|
||||
"name": "SANTIAGO-UN-CAMMINO-PER-RICOMINCIARE---TRL-120----26-06-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898318",
|
||||
"unique": "1788972262.898319"
|
||||
"modified_time": "2026-09-10T14:17:01.260333",
|
||||
"unique": "1789042621.260334"
|
||||
},
|
||||
{
|
||||
"name": "SE-DOMANI-NON-TORNO---TRL-90SEC--10-07-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898328",
|
||||
"unique": "1788972262.898328"
|
||||
"modified_time": "2026-09-10T14:17:01.260336",
|
||||
"unique": "1789042621.260336"
|
||||
},
|
||||
{
|
||||
"name": "SERPENTI---TRL-60--07-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898337",
|
||||
"unique": "1788972262.898337"
|
||||
"modified_time": "2026-09-10T14:17:01.260339",
|
||||
"unique": "1789042621.260339"
|
||||
},
|
||||
{
|
||||
"name": "SHAUN-VITA-DA-PECORA--SHAUNTHESHEE60----TRL-60----15-06-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898346",
|
||||
"unique": "1788972262.898347"
|
||||
"modified_time": "2026-09-10T14:17:01.260344",
|
||||
"unique": "1789042621.260345"
|
||||
},
|
||||
{
|
||||
"name": "SHAUN-VITA-DA-PECORA--STS30SALA_TLR----TRL-30----23-06-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898356",
|
||||
"unique": "1788972262.898356"
|
||||
"modified_time": "2026-09-10T14:17:01.260347",
|
||||
"unique": "1789042621.260347"
|
||||
},
|
||||
{
|
||||
"name": "THE-MORTUARY-ASSISTANT---TRL--05-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898365",
|
||||
"unique": "1788972262.898365"
|
||||
},
|
||||
{
|
||||
"name": "TRAIN-TO-BUSAN-RE-RELEASE--TRL-40S--15-07-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898374",
|
||||
"unique": "1788972262.898374"
|
||||
"modified_time": "2026-09-10T14:17:01.260350",
|
||||
"unique": "1789042621.26035"
|
||||
},
|
||||
{
|
||||
"name": "UNA-STORIA---TRL1---26-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898383",
|
||||
"unique": "1788972262.898383"
|
||||
"modified_time": "2026-09-10T14:17:01.260353",
|
||||
"unique": "1789042621.260353"
|
||||
},
|
||||
{
|
||||
"name": "WILDWOOD---TRL-150-SEC-B--19-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898392",
|
||||
"unique": "1788972262.898393"
|
||||
"modified_time": "2026-09-10T14:17:01.260356",
|
||||
"unique": "1789042621.260356"
|
||||
},
|
||||
{
|
||||
"name": "WILDWOOD---TRL-90-SEC-B--19-08-2026-",
|
||||
"modified_time": "2026-09-09T18:44:22.898401",
|
||||
"unique": "1788972262.898402"
|
||||
"modified_time": "2026-09-10T14:17:01.260358",
|
||||
"unique": "1789042621.260359"
|
||||
}
|
||||
],
|
||||
"notify_filters": [
|
||||
@@ -222,12 +202,12 @@
|
||||
"dirs": [
|
||||
{
|
||||
"name": ".",
|
||||
"modified_time": "2026-09-09T15:41:58",
|
||||
"modified_time": "2026-09-10T09:12:44",
|
||||
"unique": "FD02UF00001"
|
||||
},
|
||||
{
|
||||
"name": "..",
|
||||
"modified_time": "2026-09-09T15:41:58",
|
||||
"modified_time": "2026-09-10T09:12:44",
|
||||
"unique": "FD02UF00001"
|
||||
},
|
||||
{
|
||||
@@ -515,6 +495,16 @@
|
||||
"modified_time": "2026-07-24T09:42:22",
|
||||
"unique": "FD02UF1006C"
|
||||
},
|
||||
{
|
||||
"name": "FEFTRLA_TLR_F-185_IT-XX_IT_51_2K_UP_20260625_PBC_SMPTE_OV",
|
||||
"modified_time": "2026-09-10T07:42:38",
|
||||
"unique": "FD02UF1007C"
|
||||
},
|
||||
{
|
||||
"name": "FEFTRLA_TLR_S-239_IT-XX_IT_51_2K_UP_20260625_PBC_SMPTE_OV",
|
||||
"modified_time": "2026-09-10T07:42:38",
|
||||
"unique": "FD02UF1007D"
|
||||
},
|
||||
{
|
||||
"name": "FIP60ANTEPRIMA_TLR_F-185_IT-IT_IT_51_2K_EAG_20260720_PBC_IOP_OV",
|
||||
"modified_time": "2026-07-20T16:54:51",
|
||||
@@ -550,6 +540,16 @@
|
||||
"modified_time": "2026-07-31T12:16:12",
|
||||
"unique": "FD02UF10079"
|
||||
},
|
||||
{
|
||||
"name": "Goliath_TLR-G_F_IT-XX_IT_51_2K_SPE_20260908_DMK_IOP_OV",
|
||||
"modified_time": "2026-09-10T09:12:44",
|
||||
"unique": "FD02UF10081"
|
||||
},
|
||||
{
|
||||
"name": "Goliath_TLR-G_S_IT-XX_IT_51_2K_SPE_20260908_DMK_IOP_OV",
|
||||
"modified_time": "2026-09-10T09:12:25",
|
||||
"unique": "FD02UF10080"
|
||||
},
|
||||
{
|
||||
"name": "JESOPAZZO_TLR-120_F_IT-XX_51_2K_01_20260730_MAF_IOP_OV",
|
||||
"modified_time": "2026-08-03T14:32:41",
|
||||
@@ -710,6 +710,16 @@
|
||||
"modified_time": "2026-09-09T10:21:47",
|
||||
"unique": "FD02UF10060"
|
||||
},
|
||||
{
|
||||
"name": "Remain_TLR-F2_F_IT-XX_IT_51_4K_WR_20260907_PXL_SMPTE_OV",
|
||||
"modified_time": "2026-09-10T08:12:13",
|
||||
"unique": "FD02UF1007F"
|
||||
},
|
||||
{
|
||||
"name": "Remain_TLR-F2_S_IT-XX_IT_51_4K_WR_20260907_PXL_SMPTE_OV",
|
||||
"modified_time": "2026-09-10T08:11:55",
|
||||
"unique": "FD02UF1007E"
|
||||
},
|
||||
{
|
||||
"name": "ResidentEvil_TLR-H_F_IT-XX_IT_51_2K_SPE_20260723_DMK_IOP_OV",
|
||||
"modified_time": "2026-07-23T16:13:15",
|
||||
|
||||
+73
-13
@@ -1,19 +1,24 @@
|
||||
from datetime import datetime, time, date
|
||||
|
||||
from typing import Dict, List, Any, ClassVar, Optional
|
||||
from typing import Dict, List, Any, ClassVar, Optional, Union, Generic, TypeVar
|
||||
from uuid import UUID
|
||||
from pydantic import BaseModel, ConfigDict, model_validator, field_validator, Field
|
||||
from pydantic import BaseModel, ConfigDict, model_validator, field_validator, Field, field_serializer
|
||||
|
||||
class MessageHeaderWrapper(BaseModel):
|
||||
Id: int
|
||||
Type: str
|
||||
Timestamp: datetime
|
||||
Timestamp: datetime = Field(default_factory=datetime.now)
|
||||
Source: str
|
||||
|
||||
@field_validator('Timestamp', mode='before')
|
||||
@classmethod
|
||||
def to_datetime(cls, value: str) -> datetime:
|
||||
return datetime.fromtimestamp(int(value)/1000)
|
||||
|
||||
@field_serializer('Timestamp')
|
||||
@classmethod
|
||||
def to_timestamp(cls, value: datetime) -> int:
|
||||
return int(value.timestamp() * 1000)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, MessageHeaderWrapper):
|
||||
@@ -23,13 +28,19 @@ class MessageHeaderWrapper(BaseModel):
|
||||
self.model_dump(exclude={"Timestamp"})
|
||||
== other.model_dump(exclude={"Timestamp"})
|
||||
)
|
||||
|
||||
class MessageBodyWrapper(BaseModel):
|
||||
...
|
||||
|
||||
V = TypeVar("V", bound=MessageBodyWrapper)
|
||||
|
||||
class SMSMessage(BaseModel):
|
||||
class SMSMessageWrapper(BaseModel, Generic[V]):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
path: ClassVar[List[str]]
|
||||
params: ClassVar[Dict[str, str]] = {}
|
||||
|
||||
MessageHeader: MessageHeaderWrapper
|
||||
MessageBody: Optional[V] = None
|
||||
|
||||
class DCPInfoWrapper(BaseModel):
|
||||
ID: UUID
|
||||
@@ -43,7 +54,7 @@ class DCPInfoWrapper(BaseModel):
|
||||
IsPlayable: bool
|
||||
IsTransferred: bool
|
||||
|
||||
class DCPInfoList(SMSMessage):
|
||||
class DCPInfoList(SMSMessageWrapper):
|
||||
DCPInfo: List[DCPInfoWrapper] | None
|
||||
|
||||
path = ['content', 'dcp', 'info', 'list']
|
||||
@@ -53,7 +64,7 @@ class PowerStatusWrapper(BaseModel):
|
||||
Device: str
|
||||
State: str
|
||||
|
||||
class PowerStatus(SMSMessage):
|
||||
class PowerStatus(SMSMessageWrapper):
|
||||
PowerStatus: List[PowerStatusWrapper]
|
||||
|
||||
path = ['status', 'sms', 'powerstatus']
|
||||
@@ -76,7 +87,7 @@ class ShowStatusDetailWrapper(BaseModel):
|
||||
def extract(cls, data: str) -> List[int]:
|
||||
return [int(v) for v in data.split(',')]
|
||||
|
||||
class ShowStatus(SMSMessage):
|
||||
class ShowStatus(SMSMessageWrapper):
|
||||
PlayState: str
|
||||
ShowStatusDetail: ShowStatusDetailWrapper
|
||||
PlayBackMode: str
|
||||
@@ -121,14 +132,14 @@ type JobProgressElem = JobProgress
|
||||
class JobProgressWrapper(BaseModel):
|
||||
JobProgress: List[JobProgressElem] | JobProgressElem
|
||||
|
||||
class DCPImportJobList(SMSMessage):
|
||||
class DCPImportJobList(SMSMessageWrapper):
|
||||
IsPaused: bool
|
||||
JobProgressList: Optional[JobProgressWrapper] = None
|
||||
|
||||
path = ['content', 'dcp', 'command']
|
||||
params = {'action': 'ListImportJobs'}
|
||||
|
||||
class StorageInfo(SMSMessage):
|
||||
class StorageInfo(SMSMessageWrapper):
|
||||
FileSystem: str
|
||||
TotalCapacity: int
|
||||
SpaceInUse: int
|
||||
@@ -168,12 +179,12 @@ class SPLMetadata(BaseModel):
|
||||
return time.fromisoformat(value)
|
||||
|
||||
type SPLMetadataElem = SPLMetadata
|
||||
class SPLList(SMSMessage):
|
||||
class SPLList(SMSMessageWrapper):
|
||||
path = ['show','playlist','info','list']
|
||||
|
||||
SPLMetadata: List[SPLMetadataElem]
|
||||
|
||||
class PrjLampInformation(SMSMessage):
|
||||
class PrjLampInformation(SMSMessageWrapper):
|
||||
path = ['command', 'projector', 'maintenance', 'lamp', 'info', 'get']
|
||||
|
||||
ProjectorId: Optional[int] = None
|
||||
@@ -195,7 +206,7 @@ class HDDStatusWrapper(BaseModel):
|
||||
Status: str
|
||||
|
||||
type HDDStatusElem = HDDStatusWrapper
|
||||
class RAIDStatusDetail(SMSMessage):
|
||||
class RAIDStatusDetail(SMSMessageWrapper):
|
||||
RAIDStatus: str
|
||||
HDDStatus: HDDStatusElem
|
||||
|
||||
@@ -219,8 +230,57 @@ class ShowScheduleEvent(BaseModel):
|
||||
IsRecurringEvent: bool
|
||||
RecurrenceRule: Optional[RecurrenceWrapper] = None
|
||||
|
||||
class ShowScheduleEventList(SMSMessage):
|
||||
class ShowScheduleEventList(SMSMessageWrapper):
|
||||
path = ['show', 'schedule', 'info', 'events', 'list']
|
||||
|
||||
ShowScheduleEventList: List[ShowScheduleEvent]
|
||||
|
||||
|
||||
###### POST COMMANDS #####
|
||||
class SMSMessage(BaseModel):
|
||||
SMSMessage: SMSMessageWrapper
|
||||
|
||||
class ParameterWrapper(BaseModel):
|
||||
Name: str
|
||||
Value: Union[str, int, bool]
|
||||
|
||||
class ParameterListWrapper(BaseModel):
|
||||
Parameter: List[ParameterWrapper]
|
||||
|
||||
class CommandWrapper(BaseModel):
|
||||
Operation: str
|
||||
ParameterList: ParameterListWrapper
|
||||
|
||||
class CommandBodyWrapper(MessageBodyWrapper):
|
||||
Command: CommandWrapper
|
||||
|
||||
class AddImportJob(SMSMessageWrapper):
|
||||
path = ['content', 'dcp', 'command']
|
||||
|
||||
MessageHeader: MessageHeaderWrapper = MessageHeaderWrapper (
|
||||
Id=-1,
|
||||
Type="Command",
|
||||
Source="LSM-100v2"
|
||||
)
|
||||
|
||||
MessageBody: Optional[MessageBodyWrapper] = CommandBodyWrapper(
|
||||
Command = CommandWrapper(
|
||||
Operation="AddImportJob",
|
||||
ParameterList=ParameterListWrapper(
|
||||
Parameter=[
|
||||
ParameterWrapper(Name="Resume", Value=True),
|
||||
ParameterWrapper(Name="Validate", Value=True)
|
||||
])
|
||||
))
|
||||
|
||||
class ErrorResponseContentWrapper(BaseModel):
|
||||
Severity: str
|
||||
Code: int
|
||||
Message: str
|
||||
Solution: Optional[str]
|
||||
|
||||
class ErrorResponseWrapper(BaseModel):
|
||||
ErrorResponse: ErrorResponseContentWrapper
|
||||
|
||||
class ErrorResponse(SMSMessageWrapper):
|
||||
MessageBody: Optional[ErrorResponseWrapper] = None
|
||||
+69
-22
@@ -2,26 +2,38 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import List, Type, TypeVar, Set, Any
|
||||
import json
|
||||
import xmltodict
|
||||
from typing import List, Type, TypeVar, Set, Any, Dict
|
||||
from uuid import UUID
|
||||
|
||||
import projrequest
|
||||
from commands import ShowStatus, SMSMessage, SPLList, SPLMetadata, StorageInfo, PrjLampInformation
|
||||
from commands import ShowStatus, SMSMessageWrapper, SMSMessage, CommandBodyWrapper, SPLList, SPLMetadata, StorageInfo, PrjLampInformation, AddImportJob, ParameterWrapper, ErrorResponse
|
||||
|
||||
from paho.mqtt import client as mqtt_client
|
||||
from paho.mqtt.enums import CallbackAPIVersion
|
||||
from requests import Timeout, ConnectionError
|
||||
from pyutils.utils import SignalHandler
|
||||
from homeassistant import HAdevice, HAsensor, HAbutton
|
||||
from ftpexplorer import check_new_titles
|
||||
from ftpexplorer import check_new_titles, FTPServers, FTPServer, NotificationMessage
|
||||
|
||||
T = TypeVar("T", bound=SMSMessage)
|
||||
T = TypeVar("T", bound=SMSMessageWrapper)
|
||||
|
||||
# Get environment variables
|
||||
env = dict(os.environ)
|
||||
LOGGER: logging.Logger
|
||||
|
||||
DISCOVERY_PREFIX = "homeassistant"
|
||||
INTERVAL = int(env['INTERVAL'])
|
||||
DCP_INTERVAL = int(env['DCP_INTERVAL'])
|
||||
DCP_SERVER_FILE = str(env['DCP_SERVER_FILE'])
|
||||
DCP_NEW_TOPIC = str(env['DCP_NEW_TOPIC'])
|
||||
|
||||
projector: projrequest.ProjectorConnection = projrequest.ProjectorConnection(
|
||||
ip=env['PROJECTOR_IP'],
|
||||
username=env['PROJECTOR_USER'],
|
||||
password=env['PROJECTOR_PASSWORD']
|
||||
)
|
||||
|
||||
def on_shutwown(client: mqtt_client.Client, userdata: Any, msg: mqtt_client.MQTTMessage) -> None:
|
||||
if msg and msg.payload.decode() == "shutdown":
|
||||
@@ -33,14 +45,51 @@ def on_shutwown(client: mqtt_client.Client, userdata: Any, msg: mqtt_client.MQTT
|
||||
)
|
||||
projector.shutdown()
|
||||
|
||||
def on_command(client: mqtt_client.Client, userdata: Any, msg: mqtt_client.MQTTMessage) -> None:
|
||||
try:
|
||||
content: Dict[str, Any] = json.loads(msg.payload.decode('utf-8'))
|
||||
cmd: str = content["cmd"]
|
||||
params: str = content["params"]
|
||||
match cmd:
|
||||
case "ingest":
|
||||
notify: NotificationMessage = NotificationMessage.model_validate(params)
|
||||
with open(DCP_SERVER_FILE, 'r') as fd:
|
||||
dcp_servers: FTPServers = FTPServers.model_validate_json(fd.read().encode('utf-8'))
|
||||
this_server: FTPServer = list(filter(lambda s: s.name == notify.server, dcp_servers.servers))[0]
|
||||
user_pswd: str = f"{this_server.username}:{this_server.password}" if this_server.username else "anonymous:"
|
||||
ingest_command: AddImportJob = AddImportJob()
|
||||
assert isinstance(ingest_command.MessageBody, CommandBodyWrapper)
|
||||
ingest_command.MessageBody.Command.ParameterList.Parameter.insert(0,
|
||||
ParameterWrapper(
|
||||
Name="Folder",
|
||||
Value=f"ftp://{user_pswd}@{this_server.host}:{this_server.port}/{notify.name}"
|
||||
)
|
||||
)
|
||||
rv: Dict = projector.post(
|
||||
path=ingest_command.path,
|
||||
data=xmltodict.unparse(
|
||||
SMSMessage(
|
||||
SMSMessage=ingest_command
|
||||
).model_dump(), indent=1)
|
||||
)
|
||||
error_response = ErrorResponse.model_validate(rv['SMSMessage'])
|
||||
if not error_response.MessageBody:
|
||||
LOGGER.warning("Invalid error response: [%s]", rv)
|
||||
return
|
||||
if error_response.MessageBody.ErrorResponse.Code != 100:
|
||||
LOGGER.error("Unable to ingest Title: [%s]", notify.name)
|
||||
else:
|
||||
LOGGER.info("Started ingest of Title: [%s] from [%s]", notify.name, notify.server)
|
||||
|
||||
except json.JSONDecodeError as de:
|
||||
LOGGER.error("Unable to decode command json: [%s]", de)
|
||||
except KeyError as ke:
|
||||
LOGGER.error("Unable to find key: [%s]", ke)
|
||||
|
||||
##################
|
||||
###### MAIN ######
|
||||
##################
|
||||
def main() -> int:
|
||||
INTERVAL = int(env['INTERVAL'])
|
||||
DCP_INTERVAL = int(env['DCP_INTERVAL'])
|
||||
DCP_SERVER_FILE = str(env['DCP_SERVER_FILE'])
|
||||
DCP_NEW_TOPIC = str(env['DCP_NEW_TOPIC'])
|
||||
try:
|
||||
mqtt = mqtt_client.Client(CallbackAPIVersion.VERSION2)
|
||||
mqtt.username_pw_set(username=env['MQTT_USER'], password=env['MQTT_PASSWORD'])
|
||||
@@ -58,12 +107,7 @@ def main() -> int:
|
||||
last: float = 0
|
||||
last_title_check: float = 0
|
||||
handler: SignalHandler = SignalHandler(LOGGER)
|
||||
projector: projrequest.ProjectorConnection = projrequest.ProjectorConnection(
|
||||
ip=env['PROJECTOR_IP'],
|
||||
username=env['PROJECTOR_USER'],
|
||||
password=env['PROJECTOR_PASSWORD']
|
||||
)
|
||||
|
||||
|
||||
sony_device: HAdevice = HAdevice (
|
||||
identifiers=["sony_digitalcinema_projector"],
|
||||
name="Sony Digital Cinema Projector",
|
||||
@@ -75,7 +119,7 @@ def main() -> int:
|
||||
sony_device.entities.append(HAbutton (
|
||||
name="Power Off",
|
||||
unique_id=f"{sony_device.short_name}_power_off",
|
||||
command_topic=f"{sony_device.short_name}/commands",
|
||||
command_topic=f"{sony_device.short_name}/shutdown",
|
||||
icon="mdi:power",
|
||||
payload_press="shutdown",
|
||||
entity_type="button",
|
||||
@@ -243,8 +287,8 @@ def main() -> int:
|
||||
|
||||
lamp_devices.append(lamp_device)
|
||||
|
||||
# Subscribe to MQTT topics for devices having associate actions
|
||||
all_devices: List[HAdevice] = [sony_device, *lamp_devices]
|
||||
|
||||
for d in all_devices:
|
||||
for s in d.entities:
|
||||
# Publish config mesasge
|
||||
@@ -257,13 +301,16 @@ def main() -> int:
|
||||
if s.callback is not None:
|
||||
mqtt.message_callback_add(sub=s.command_topic, callback=s.callback)
|
||||
time.sleep(0.2)
|
||||
|
||||
# Subscrie to MQTT topics for generic commands
|
||||
mqtt.subscribe(topic=f"{sony_device.short_name}/commands")
|
||||
mqtt.message_callback_add(sub=f"{sony_device.short_name}/commands", callback=on_command)
|
||||
|
||||
playlists: SPLList | None = None
|
||||
|
||||
while handler.running:
|
||||
now: float = time.time()
|
||||
try:
|
||||
resp: None | SMSMessage = None
|
||||
resp: None | SMSMessageWrapper = None
|
||||
# update projector states
|
||||
lamp_num: int = 0
|
||||
for d in all_devices:
|
||||
@@ -295,13 +342,13 @@ def main() -> int:
|
||||
resp.ShowStatusDetail.CurrentEventTitle = "Unknown or CPL"
|
||||
|
||||
# send only one message for sensor with this type of request
|
||||
last_cls: Type = SMSMessage
|
||||
last_cls: Type = SMSMessageWrapper
|
||||
# filter sensors of this device per this type of request
|
||||
for sens in filter(lambda x: x.request == s, sensors):
|
||||
if last_cls == sens.request:
|
||||
continue
|
||||
if isinstance(resp, SMSMessage):
|
||||
sts: SMSMessage = resp
|
||||
if isinstance(resp, SMSMessageWrapper):
|
||||
sts: SMSMessageWrapper = resp
|
||||
mqtt.publish(topic=sens.state_topic,
|
||||
payload=sts.model_dump_json(
|
||||
indent=1,
|
||||
@@ -309,7 +356,7 @@ def main() -> int:
|
||||
exclude_unset=True))
|
||||
last_cls = sens.request
|
||||
except (Timeout, ConnectionError) as ce:
|
||||
LOGGER.debug("Projector is probably shutdown: %s", ce)
|
||||
LOGGER.debug("Projector is probably shutdown: [%s]", ce)
|
||||
except Exception as e:
|
||||
LOGGER.error("Unexpected exception: [%s]", e)
|
||||
return 1
|
||||
|
||||
+21
-2
@@ -7,10 +7,10 @@ from os import getcwd
|
||||
from typing import Any, Dict, List, Type, TypeVar
|
||||
|
||||
import xmltodict
|
||||
from commands import SMSMessage
|
||||
from commands import SMSMessageWrapper
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
T = TypeVar("T", bound=SMSMessage)
|
||||
T = TypeVar("T", bound=SMSMessageWrapper)
|
||||
|
||||
TEST = False
|
||||
|
||||
@@ -25,6 +25,24 @@ class ProjectorConnection():
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.auth = HTTPBasicAuth(username=self.username, password=self.password)
|
||||
|
||||
def post(self, path: List[str], params: Dict[str, str] | None = None, data: str | None = None) -> Dict[str, Any]:
|
||||
response: requests.Response = requests.post(
|
||||
url=f"https://{self.ip}/{"/".join(path)}",
|
||||
params=params,
|
||||
headers={
|
||||
"Accept": "application/xml"
|
||||
},
|
||||
auth=self.auth,
|
||||
allow_redirects=False,
|
||||
timeout=5,
|
||||
verify=False,
|
||||
data=data
|
||||
)
|
||||
response.raise_for_status()
|
||||
if response.status_code == 200: # HTTP ok response
|
||||
return xmltodict.parse(response.text.replace("%2F", "/").replace("%3A", ":")) # Common containert for all messages
|
||||
return {}
|
||||
|
||||
def get(self, path: List[str], params: Dict[str, str] | None = None) -> Dict[str, Any]:
|
||||
response: requests.Response = requests.get(
|
||||
@@ -56,6 +74,7 @@ class ProjectorConnection():
|
||||
rv.update(**rv['MessageBody'][tt])
|
||||
del rv['MessageBody']
|
||||
del rv['@xmlns']
|
||||
rv.update(**rv)
|
||||
#print(json.dumps(rv, indent = 2))
|
||||
return cls.model_validate(rv)
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<!--
|
||||
{
|
||||
"POST": {
|
||||
"scheme": "https",
|
||||
"host": "192.168.31.10",
|
||||
"filename": "/content/dcp/command",
|
||||
"remote": {
|
||||
"Indirizzo": "192.168.31.10:443"
|
||||
}
|
||||
}
|
||||
}
|
||||
-->
|
||||
|
||||
<!-- request -->
|
||||
<SMSMessage>
|
||||
<MessageHeader>
|
||||
<Id>-1</Id>
|
||||
<Type>Command</Type>
|
||||
<Timestamp>1789025739512</Timestamp>
|
||||
<Source>LSM-100v2</Source>
|
||||
</MessageHeader>
|
||||
<MessageBody>
|
||||
<Command>
|
||||
<Operation>AddImportJob</Operation>
|
||||
<ParameterList>
|
||||
<Parameter>
|
||||
<Name>Folder</Name>
|
||||
<Value>ftp://dcpftp:dcpftp@192.168.31.16:21/1874853_LinkinParkU_TLR-Ext_F_EN-it_IT_51_4K_NXO_20260908_DLX_IOP_OV</Value>
|
||||
</Parameter>
|
||||
<Parameter>
|
||||
<Name>Resume</Name>
|
||||
<Value>true</Value>
|
||||
</Parameter>
|
||||
<Parameter>
|
||||
<Name>Validate</Name>
|
||||
<Value>true</Value>
|
||||
</Parameter>
|
||||
</ParameterList>
|
||||
</Command>
|
||||
</MessageBody>
|
||||
</SMSMessage>
|
||||
|
||||
<!-- response -->
|
||||
<SMSMessage xmlns="http://xmlns.sony.net/d-cinema/sms/2007b">
|
||||
<MessageHeader>
|
||||
<Id>-1</Id>
|
||||
<Type>ErrorResponse</Type>
|
||||
<Timestamp>1789025967924</Timestamp>
|
||||
<Source>LSM-100v2</Source>
|
||||
</MessageHeader>
|
||||
<MessageBody>
|
||||
<ErrorResponse>
|
||||
<Severity>INFO</Severity>
|
||||
<Code>100</Code>
|
||||
<Message>Request+successfully+processed.</Message>
|
||||
<Solution></Solution>
|
||||
</ErrorResponse>
|
||||
</MessageBody>
|
||||
</SMSMessage>
|
||||
|
||||
|
||||
<!-- MY REQUEST -->
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<SMSMessage>
|
||||
<MessageHeader>
|
||||
<Id>-1</Id>
|
||||
<Type>Command</Type>
|
||||
<Timestamp>1789038719365</Timestamp>
|
||||
<Source>LSM-100v2</Source>
|
||||
</MessageHeader>
|
||||
<MessageBody>
|
||||
<Command>
|
||||
<Operation>AddImportJob</Operation>
|
||||
<ParameterList>
|
||||
<Name>Resume</Name>
|
||||
<Value>true</Value>
|
||||
</ParameterList>
|
||||
<ParameterList>
|
||||
<Name>Validate</Name>
|
||||
<Value>true</Value>
|
||||
</ParameterList>
|
||||
<ParameterList>
|
||||
<Name>Folder</Name>
|
||||
<Value>ftp://dcpftp:dcpftp@192.168.31.16:21/Remain_TLR-F2_S_IT-XX_IT_51_4K_WR_20260907_PXL_SMPTE_OV</Value>
|
||||
</ParameterList>
|
||||
</Command>
|
||||
</MessageBody>
|
||||
</SMSMessage>
|
||||
Reference in New Issue
Block a user