Compare commits

..

2 Commits

8 changed files with 217 additions and 80 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",

View File

@@ -1,61 +1,55 @@
from projrequest import ProjectorConnection
from datetime import datetime from datetime import datetime
from pydantic import BaseModel
from typing import Dict, List, Any from typing import Dict, List, Any, ClassVar, Optional
from uuid import UUID from uuid import UUID
from pydantic import BaseModel, ConfigDict, model_validator, field_validator
class BaseCommand(BaseModel): class MessageHeaderWrapper(BaseModel):
projector: ProjectorConnection Id: int
timestamp: datetime Type: str
type: str Timestamp: datetime
content: Any Source: str
def __init__(self, proj: ProjectorConnection) -> None: @field_validator('Timestamp', mode='before')
self.projector = proj @classmethod
pass def to_datetime(cls, value: str) -> datetime:
return datetime.fromtimestamp(int(value)/1000)
def update(self, path: List[str], params: Dict[str, Any] | None = None): class SMSMessage(BaseModel):
resp = self.projector.get(path=path, params=params) model_config = ConfigDict(arbitrary_types_allowed=True)
if resp is not None: path: ClassVar[List[str]]
self.timestamp = datetime.fromtimestamp(float(resp['timestamp'])) params: ClassVar[Dict[str, Any] | None] = None
self.type = resp['type']
self.content = resp['body'][resp['type']] MessageHeader: MessageHeaderWrapper
class DCPInfo(BaseModel): class DCPInfoWrapper(BaseModel):
ID: UUID ID: UUID
Title: str Title: str
Path: str Path: str
Size: int Size: int
ImportTime: datetime ImportTime: datetime
IsImported: bool IsImported: bool
VerifyStatus: bool VerifyStatus: str
ValidateStatus: bool ValidateStatus: str
IsPlayable: bool IsPlayable: bool
IsTransferred: bool IsTransferred: bool
class DCPInfoList(BaseCommand): class DCPInfoList(SMSMessage):
dcpInfoList: List[DCPInfo] DCPInfo: List[DCPInfoWrapper] | None
path: List[str] = ['content', 'dcp', 'info', 'list']
params: Dict[str, str] = { 'formatDate': 'false' } path = ['content', 'dcp', 'info', 'list']
params = { 'formatDate': 'false' }
def get(self): class PowerStatusWrapper(BaseModel):
self.update(path=self.path, params=self.params)
self.dcpInfoList = [DCPInfo(**e) for e in self.content]
class PowerStatus(BaseModel):
Device: str Device: str
State: str State: str
class PowerStatusList(BaseCommand): class PowerStatus(SMSMessage):
powerStatusList: List[PowerStatus] PowerStatus: List[PowerStatusWrapper]
path: List[str] = ['status', 'sms', 'powerstatus']
def get(self): path = ['status', 'sms', 'powerstatus']
self.update(path=self.path)
self.powerStatusList = [PowerStatus(**e) for e in self.content]
class ShowStatusDetailClass(BaseModel): class ShowStatusDetailWrapper(BaseModel):
Type: str Type: str
Id: UUID Id: UUID
RemainingTime: int RemainingTime: int
@@ -64,34 +58,33 @@ class ShowStatusDetailClass(BaseModel):
CurrentEventId: UUID CurrentEventId: UUID
CurrentEventType: str CurrentEventType: str
IsStoppedByMalfunction: bool IsStoppedByMalfunction: bool
RewindTimeList: str RewindTimeList: List[int]
MalfunctionTime: int MalfunctionTime: int
class ShowStatus(BaseCommand): @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 PlayState: str
ShowStatusDetail: ShowStatusDetailClass ShowStatusDetail: ShowStatusDetailWrapper
PlayBackMode: str PlayBackMode: str
AtmosPlayingStatus: str AtmosPlayingStatus: str
path: List[str] = ['playback', 'showstatus']
def get(self): path = ['playback', 'showstatus']
self.update(self.path)
self.PlayState = self.content['PlayState']
self.ShowStatusDetail = ShowStatusDetailClass(**self.content['StatusDetail'])
self.PlayBackMode = self.content['PlayBackMode']
self.AtmosPlayingStatus = self.content['AtmosPlayingStatus']
class ImportProgressClass(BaseModel): class ImportProgressWrapper(BaseModel):
TotalBytesToTransfer: int TotalBytesToTransfer: int
BytesTransferred: int BytesTransferred: int
PercentCompleted: int PercentCompleted: int
InProgress: int InProgress: bool
ImportPath: str ImportPath: str
CompletionStatus: str CompletionStatus: str
CompletionTime: str CompletionTime: Optional[datetime] = None
DCPTitle: str DCPTitle: Optional[str] = None
class ValidationProgressClass(BaseModel): class ValidationProgress(BaseModel):
TotalBytesToValidate: int TotalBytesToValidate: int
BytesValidated: int BytesValidated: int
PercentCompleted: int PercentCompleted: int
@@ -100,23 +93,44 @@ class ValidationProgressClass(BaseModel):
CompletionStatus: str CompletionStatus: str
CompletionTime: datetime CompletionTime: datetime
type ValidationProgressElem = ValidationProgress
class ValidationProgressWrapper(BaseModel):
ValidationProgress: ValidationProgressElem
class JobProgress(BaseModel): class JobProgress(BaseModel):
Id: int Id: int
ValidateAfterImport: bool ValidateAfterImport: bool
AggregatePercentValidated: int AggregatePercentValidated: int
State: str State: str
ImportProgress: ImportProgressClass ImportProgress: ImportProgressWrapper
ValidationProgressList: List[ValidationProgressClass] ValidationProgressList: ValidationProgressWrapper | None
IngestedByFolder: bool IngestedByFolder: bool
ContentsTransferType: str ContentsTransferType: str
class DCPImportJobList(BaseCommand): type JobProgressElem = JobProgress
IsPaused: bool class JobProgressWrapper(BaseModel):
JobProgressList: List[JobProgress] JobProgress: List[JobProgressElem] | JobProgressElem
path: List[str] = ['content', 'dcp', 'command']
params: Dict[str, str] = {'action': 'ListImportJobs'}
def get(self): class DCPImportJobList(SMSMessage):
self.update(self.path) IsPaused: bool
self.IsPaused = self.content['IsPaused'] JobProgressList: JobProgressWrapper | None
self.JobProgressList = [JobProgress(**e) for e in self.content['JobProgressList']]
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,19 +39,17 @@ 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()
ret = projector.request(ShowStatus)
resp = projector.get(path=['status', 'storage', 'info'], params={"area":"DCP"}) print(ret)
if resp is not None:
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)
except Exception as e: except Exception as e:
print(f"Unexpected exception: [{e}]") print(f"Unexpected exception: [{e}]")
return 1 return 1

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>