ingest command working

This commit is contained in:
2026-09-10 14:22:02 +02:00
parent fec8259854
commit c349863611
5 changed files with 353 additions and 129 deletions
+102 -92
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+88
View File
@@ -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>