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",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"program": "${workspaceFolder}/projmon/projmon.py",
"console": "integratedTerminal",
"env": {
"INTERVAL" : "5",

View File

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

View File

@@ -1,9 +1,18 @@
import requests
import urllib3
# Suppress only the single warning from urllib3.
urllib3.disable_warnings(category=urllib3.exceptions.InsecureRequestWarning)
import xmltodict
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():
ip: str
@@ -30,13 +39,24 @@ class ProjectorConnection():
timeout=4,
verify=False
)
response.raise_for_status()
if response.status_code == 200: # HTTP ok response
rv: dict = {}
content: dict = xmltodict.parse(response.text)["SMSMessage"] # Common containert for all messages
header: dict = content["MessageHeader"]
body: dict = content["MessageBody"]
rv['type'] = header.get("Type", None)
rv['timestamp'] = int(header.get("Timestamp", datetime.now()))
rv['body'] = body[rv["type"]]
return rv
return xmltodict.parse(response.text.replace("%2F", "/").replace("%3A", ":")) # Common containert for all messages
return None
def request(self, cls: Type[T]) -> T | None:
rv: dict | None
if TEST:
with open(f'{getcwd()}/projmon/xmlresponses/showstatus.xml', 'r') as fd:
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

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>