Compare commits

...

4 Commits

8 changed files with 277 additions and 16 deletions

2
.vscode/launch.json vendored
View File

@@ -43,7 +43,7 @@
"name": "Python Debugger: PROJmon", "name": "Python Debugger: PROJmon",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "${file}", "program": "${workspaceFolder}/projmon/projmon.py",
"console": "integratedTerminal", "console": "integratedTerminal",
"env": { "env": {
"INTERVAL" : "5", "INTERVAL" : "5",

136
projmon/commands.py Normal file
View File

@@ -0,0 +1,136 @@
from datetime import datetime
from typing import Dict, List, Any, ClassVar, Optional
from uuid import UUID
from pydantic import BaseModel, ConfigDict, model_validator, field_validator
class MessageHeaderWrapper(BaseModel):
Id: int
Type: str
Timestamp: datetime
Source: str
@field_validator('Timestamp', mode='before')
@classmethod
def to_datetime(cls, value: str) -> datetime:
return datetime.fromtimestamp(int(value)/1000)
class SMSMessage(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
path: ClassVar[List[str]]
params: ClassVar[Dict[str, Any] | None] = None
MessageHeader: MessageHeaderWrapper
class DCPInfoWrapper(BaseModel):
ID: UUID
Title: str
Path: str
Size: int
ImportTime: datetime
IsImported: bool
VerifyStatus: str
ValidateStatus: str
IsPlayable: bool
IsTransferred: bool
class DCPInfoList(SMSMessage):
DCPInfo: List[DCPInfoWrapper] | None
path = ['content', 'dcp', 'info', 'list']
params = { 'formatDate': 'false' }
class PowerStatusWrapper(BaseModel):
Device: str
State: str
class PowerStatus(SMSMessage):
PowerStatus: List[PowerStatusWrapper]
path = ['status', 'sms', 'powerstatus']
class ShowStatusDetailWrapper(BaseModel):
Type: str
Id: UUID
RemainingTime: int
ElapsedTime: int
TotalDuration: int
CurrentEventId: UUID
CurrentEventType: str
IsStoppedByMalfunction: bool
RewindTimeList: List[int]
MalfunctionTime: int
@field_validator('RewindTimeList', mode='before')
@classmethod
def extract(cls, data: str) -> List[int]:
return [int(v) for v in data.split(',')]
class ShowStatus(SMSMessage):
PlayState: str
ShowStatusDetail: ShowStatusDetailWrapper
PlayBackMode: str
AtmosPlayingStatus: str
path = ['playback', 'showstatus']
class ImportProgressWrapper(BaseModel):
TotalBytesToTransfer: int
BytesTransferred: int
PercentCompleted: int
InProgress: bool
ImportPath: str
CompletionStatus: str
CompletionTime: Optional[datetime] = None
DCPTitle: Optional[str] = None
class ValidationProgress(BaseModel):
TotalBytesToValidate: int
BytesValidated: int
PercentCompleted: int
InProgress: bool
Id: UUID
CompletionStatus: str
CompletionTime: datetime
type ValidationProgressElem = ValidationProgress
class ValidationProgressWrapper(BaseModel):
ValidationProgress: ValidationProgressElem
class JobProgress(BaseModel):
Id: int
ValidateAfterImport: bool
AggregatePercentValidated: int
State: str
ImportProgress: ImportProgressWrapper
ValidationProgressList: ValidationProgressWrapper | None
IngestedByFolder: bool
ContentsTransferType: str
type JobProgressElem = JobProgress
class JobProgressWrapper(BaseModel):
JobProgress: List[JobProgressElem] | JobProgressElem
class DCPImportJobList(SMSMessage):
IsPaused: bool
JobProgressList: JobProgressWrapper | None
path = ['content', 'dcp', 'command']
params = {'action': 'ListImportJobs'}
class StorageInfo(SMSMessage):
FileSystem: str
TotalCapacity: int
SpaceInUse: int
FreeSpace: int
path = ['status', 'storage', 'info']
params = {'area': 'DCP'}
@model_validator(mode='before')
@classmethod
def extract(cls, data: Any) -> Any:
if isinstance(data, dict):
data.update(**data['MBStorageUsage'])
del data['MBStorageUsage']
return data

14
projmon/haconfig.py Normal file
View File

@@ -0,0 +1,14 @@
HA_CONFIG_TEMPLATE = {
"name": "{device_friendly_name}",
"state_topic": "homeassistant/sensor/{device_name}/{device_state}/state",
"unique_id": "{device_name}_status",
"icon": "{device_icon}",
"device": {
"identifiers": ["{device_name}"],
"name": "{device_friendly_name}",
"manufacturer": "Edelweiss_Automation",
"model": "Edelweiss_Automation"
}
}

View File

@@ -5,6 +5,7 @@ import logging
import json import json
import projrequest as projrequest import projrequest as projrequest
from commands import ShowStatus, PowerStatus, DCPImportJobList, DCPInfoList, StorageInfo
from utils import * from utils import *
from influxdb_client_3 import InfluxDBClient3, Point from influxdb_client_3 import InfluxDBClient3, Point
@@ -38,16 +39,16 @@ def main() -> int:
username=env['PROJECTOR_USER'], username=env['PROJECTOR_USER'],
password=env['PROJECTOR_PASSWORD'] password=env['PROJECTOR_PASSWORD']
) )
while handler.running: while handler.running:
try: try:
now:float = time.time() now:float = time.time()
resp = projector.get(path=['status', 'storage', 'info'], params={"area":"DCP"}) ret = projector.request(ShowStatus)
if resp is not None: print(ret)
print(json.dumps(resp, indent=2))
last: float = time.time() last: float = time.time()
cycle_time: float = last - now cycle_time: float = last - now
LOGGER.debug(f"Cycle Time: {cycle_time:4.3f}") LOGGER.debug(f"Cycle Time: {cycle_time:4.3f}")
time.sleep(INTERVAL-cycle_time) if cycle_time < INTERVAL:
time.sleep(INTERVAL-cycle_time) time.sleep(INTERVAL-cycle_time)
except Exception as e: except Exception as e:
print(f"Unexpected exception: [{e}]") print(f"Unexpected exception: [{e}]")

View File

@@ -1,9 +1,18 @@
import requests import requests
import urllib3
# Suppress only the single warning from urllib3.
urllib3.disable_warnings(category=urllib3.exceptions.InsecureRequestWarning)
import xmltodict import xmltodict
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from datetime import datetime from os import getcwd
import json
from typing import Dict, List, Any from commands import SMSMessage
from typing import Dict, List, Any, Type, TypeVar
T = TypeVar("T", bound=SMSMessage)
TEST = True
class ProjectorConnection(): class ProjectorConnection():
ip: str ip: str
@@ -30,13 +39,24 @@ class ProjectorConnection():
timeout=4, timeout=4,
verify=False verify=False
) )
response.raise_for_status()
if response.status_code == 200: # HTTP ok response if response.status_code == 200: # HTTP ok response
rv: dict = {} return xmltodict.parse(response.text.replace("%2F", "/").replace("%3A", ":")) # Common containert for all messages
content: dict = xmltodict.parse(response.text)["SMSMessage"] # Common containert for all messages return None
header: dict = content["MessageHeader"]
body: dict = content["MessageBody"] def request(self, cls: Type[T]) -> T | None:
rv['type'] = header.get("Type", None) rv: dict | None
rv['timestamp'] = int(header.get("Timestamp", datetime.now())) if TEST:
rv['body'] = body[rv["type"]] with open(f'{getcwd()}/projmon/xmlresponses/showstatus.xml', 'r') as fd:
return rv rv = xmltodict.parse(fd.read().replace("%2F", "/").replace("%3A", ":"))
else:
rv = self.get(cls.path, cls.params)
if isinstance(rv, dict):
rv = rv['SMSMessage']
type: str = rv['MessageHeader']['Type']
rv.update(**rv['MessageBody'][type])
del rv['MessageBody']
del rv['@xmlns']
print(json.dumps(rv, indent = 2))
return cls.model_validate(rv)
return None return None

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<SMSMessage xmlns="http://xmlns.sony.net/d-cinema/sms/2007b">
<MessageHeader>
<Id>-1</Id>
<Type>DCPImportJobList</Type>
<Timestamp>1771320721827</Timestamp>
<Source>LSM-100v2</Source>
</MessageHeader>
<MessageBody>
<DCPImportJobList>
<IsPaused>false</IsPaused>
<CurrentJobIdList></CurrentJobIdList>
<JobProgressList></JobProgressList>
</DCPImportJobList>
</MessageBody>
</SMSMessage>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<SMSMessage xmlns="http://xmlns.sony.net/d-cinema/sms/2007b">
<MessageHeader>
<Id>-1</Id>
<Type>DCPImportJobList</Type>
<Timestamp>1771321131757</Timestamp>
<Source>LSM-100v2</Source>
</MessageHeader>
<MessageBody>
<DCPImportJobList>
<IsPaused>true</IsPaused>
<CurrentJobIdList></CurrentJobIdList>
<JobProgressList>
<JobProgress>
<Id>0</Id>
<ValidateAfterImport>true</ValidateAfterImport>
<AggregatePercentValidated>0</AggregatePercentValidated>
<State>PAUSED</State>
<ImportProgress>
<TotalBytesToTransfer>197696878067</TotalBytesToTransfer>
<BytesTransferred>4904562323</BytesTransferred>
<PercentCompleted>2</PercentCompleted>
<InProgress>false</InProgress>
<ImportPath>ftp%3A%2F192.168.31.16%2FGRAZIA_FTR_S_IT-XX_IT_51_4K_ST_20251021_FLA_SMPTE_OV</ImportPath>
<CompletionStatus>CANCELLED</CompletionStatus>
<CompletionTime>2026-02-17T10:38:40.210+01:00</CompletionTime>
<DCPTitle>GRAZIA_FTR_S_IT-XX_IT_51_4K_ST_20251021_FLA_SMPTE_OV</DCPTitle>
</ImportProgress>
<ValidationProgressList></ValidationProgressList>
<IngestedByFolder>true</IngestedByFolder>
<ContentsTransferType>FTPINGEST</ContentsTransferType>
</JobProgress>
</JobProgressList>
</DCPImportJobList>
</MessageBody>
</SMSMessage>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<SMSMessage xmlns="http://xmlns.sony.net/d-cinema/sms/2007b">
<MessageHeader>
<Id>-1</Id>
<Type>DCPImportJobList</Type>
<Timestamp>1771321082485</Timestamp>
<Source>LSM-100v2</Source>
</MessageHeader>
<MessageBody>
<DCPImportJobList>
<IsPaused>false</IsPaused>
<CurrentJobIdList>
<Id>0</Id>
</CurrentJobIdList>
<JobProgressList>
<JobProgress>
<Id>0</Id>
<ValidateAfterImport>true</ValidateAfterImport>
<AggregatePercentValidated>0</AggregatePercentValidated>
<State>IMPORTING</State>
<ImportProgress>
<TotalBytesToTransfer>197696878067</TotalBytesToTransfer>
<BytesTransferred>998483110</BytesTransferred>
<PercentCompleted>1</PercentCompleted>
<InProgress>true</InProgress>
<ImportPath>ftp%3A%2F192.168.31.16%2FGRAZIA_FTR_S_IT-XX_IT_51_4K_ST_20251021_FLA_SMPTE_OV</ImportPath>
<CompletionStatus>NONE</CompletionStatus>
<DCPTitle>GRAZIA_FTR_S_IT-XX_IT_51_4K_ST_20251021_FLA_SMPTE_OV</DCPTitle>
</ImportProgress>
<ValidationProgressList></ValidationProgressList>
<IngestedByFolder>true</IngestedByFolder>
<ContentsTransferType>FTPINGEST</ContentsTransferType>
</JobProgress>
</JobProgressList>
</DCPImportJobList>
</MessageBody>
</SMSMessage>