Skip to content

Commit 9c17da2

Browse files
committedJun 10, 2018
Initial commit
0 parents  commit 9c17da2

7 files changed

+372
-0
lines changed
 

‎pysdcp/LICENSE.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2017 Guy Shapira
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎pysdcp/README.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
This module was cloned from https://github.com/Galala7/pySDCP and modified slightly for the MQTT-bridge
2+
3+
4+
# pySDCP
5+
Sony SDCP / PJ Talk projector control
6+
7+
Python **3** library to query and control Sony Projectors using SDCP (PJ Talk) protocol over IP.
8+
9+
##Features:
10+
* Autodiscover projector using SDAP (Simple Display Advertisement Protocol)
11+
* Query and change power status
12+
* Toggle input between HDMI-1 and HDMI-2
13+
14+
## More features
15+
The SDCP protocol allow to control practically everything in projector, i.e. resolution, brightness, 3d format...
16+
If you need to use more commands, just add to _protocol.py_, and send with _my_projector._send_command__
17+
18+
### Protocl Documnetation:
19+
* [Link](https://www.digis.ru/upload/iblock/f5a/VPL-VW320,%20VW520_ProtocolManual.pdf)
20+
* [Link](https://docs.sony.com/release//VW100_protocol.pdf)
21+
22+
23+
#Supported Projectors
24+
Supported Sony projectors should include:
25+
* VPL-VW515
26+
* VPL-VW520
27+
* VPL-VW528
28+
* VPL-VW665
29+
* VPL-VW315
30+
* VPL-VW320
31+
* VPL-VW328
32+
* VPL-VW365
33+
* VPL-VW100
34+
* VPL-HW65ES
35+
36+
## Installation
37+
```pip install pySDCP```
38+
39+
## Examples
40+
41+
42+
Sending any command will initiate autodiscovery of projector if none is known and will cary on the command. so just go for it and maybe you get lucky:
43+
```
44+
import pySDCP
45+
46+
my_projector = pySDCP.Projector()
47+
48+
my_projector.get_power()
49+
my_projector.set_power(True)
50+
```
51+
52+
Skip discovery to save time or if you know the IP of the projector
53+
```
54+
my_known_projector = pySDCP.Projector('10.1.2.3')
55+
my_known_projector.set_HDMI_input(2)
56+
```
57+
58+
# Credits
59+
This plugin is based on [sony-sdcp-com](https://github.com/vokkim/sony-sdcp-com) NodeJS library by [vokkim](https://github.com/vokkim).
60+
61+
## See also
62+
[homebridge-sony-sdcp](https://github.com/Galala7/homebridge-sony-sdcp) Homebridge plugin to control Sony Projectors.
63+

‎pysdcp/__init__.py

+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
#! py3
2+
3+
import socket
4+
from collections import namedtuple
5+
from struct import *
6+
7+
from pysdcp.protocol import *
8+
9+
Header = namedtuple("Header", ['version', 'category', 'community'])
10+
ProjInfo = namedtuple("ProjInfo", ['id', 'product_name', 'serial_number', 'power_state', 'location'])
11+
12+
13+
def create_command_buffer(header: Header, action, command, data=None):
14+
# create bytearray in the right size
15+
if data is not None:
16+
my_buf = bytearray(12)
17+
else:
18+
my_buf = bytearray(10)
19+
# header
20+
my_buf[0] = 2 # only works with version 2, don't know why
21+
my_buf[1] = header.category
22+
# community
23+
my_buf[2] = ord(header.community[0])
24+
my_buf[3] = ord(header.community[1])
25+
my_buf[4] = ord(header.community[2])
26+
my_buf[5] = ord(header.community[3])
27+
# command
28+
my_buf[6] = action
29+
pack_into(">H", my_buf, 7, command)
30+
if data is not None:
31+
# add data len
32+
my_buf[9] = 2 # Data is always 2 bytes
33+
# add data
34+
pack_into(">H", my_buf, 10, data)
35+
else:
36+
my_buf[9] = 0
37+
return my_buf
38+
39+
40+
def process_command_response(msgBuf):
41+
my_header = Header(
42+
version=int(msgBuf[0]),
43+
category=int(msgBuf[1]),
44+
community=decode_text_field(msgBuf[2:6]))
45+
is_success = bool(msgBuf[6])
46+
command = unpack(">H", msgBuf[7:9])[0]
47+
data_len = int(msgBuf[9])
48+
if data_len != 0:
49+
data = unpack(">H", msgBuf[10:10 + data_len])[0]
50+
else:
51+
data = None
52+
return my_header, is_success, command, data
53+
54+
55+
def process_SDAP(SDAP_buffer) -> (Header, ProjInfo):
56+
try:
57+
my_header = Header(
58+
version=int(SDAP_buffer[2]),
59+
category=int(SDAP_buffer[3]),
60+
community=decode_text_field(SDAP_buffer[4:8]))
61+
my_info = ProjInfo(
62+
id=SDAP_buffer[0:2].decode(),
63+
product_name=decode_text_field(SDAP_buffer[8:20]),
64+
serial_number=unpack('>I', SDAP_buffer[20:24])[0],
65+
power_state=unpack('>H', SDAP_buffer[24:26])[0],
66+
location=decode_text_field(SDAP_buffer[26:]))
67+
except Exception as e:
68+
print("Error parsing SDAP packet: {}".format(e))
69+
raise
70+
return my_header, my_info
71+
72+
73+
def decode_text_field(buf):
74+
"""
75+
Convert char[] string in buffer to python str object
76+
:param buf: bytearray with array of chars
77+
:return: string
78+
"""
79+
return buf.decode().strip(b'\x00'.decode())
80+
81+
82+
class Projector:
83+
def __init__(self, ip: str = None):
84+
"""
85+
Base class for projector communication.
86+
Enables communication with Projector, Sending commands and Querying Power State
87+
88+
:param ip: str, IP address for projector. if given, will create a projector with default values to communicate
89+
with projector on the given ip. i.e. "10.0.0.5"
90+
"""
91+
self.info = ProjInfo(
92+
product_name=None,
93+
serial_number=None,
94+
power_state=None,
95+
location=None,
96+
id=None)
97+
if ip is None:
98+
# Create empty Projector object
99+
self.ip = None
100+
self.header = Header(version=None, category=None, community=None)
101+
self.is_init = False
102+
else:
103+
# Create projector from known ip
104+
# Set default values to enable immediately communication with known project (ip)
105+
self.ip = ip
106+
self.header = Header(category=10, version=2, community="SONY")
107+
self.is_init = True
108+
109+
# Default ports
110+
self.UDP_IP = ""
111+
self.UDP_PORT = 53862
112+
self.TCP_PORT = 53484
113+
self.TCP_TIMEOUT = 2
114+
self.UDP_TIMEOUT = 31
115+
116+
def __eq__(self, other):
117+
return self.info.serial_number == other.info.serail_number
118+
119+
def _send_command(self, action, command, data=None, timeout=None):
120+
timeout = timeout if timeout is not None else self.TCP_TIMEOUT
121+
if not self.is_init:
122+
self.find_projector()
123+
if not self.is_init:
124+
raise Exception("No projector found and / or specified")
125+
126+
my_buf = create_command_buffer(self.header, action, command, data)
127+
128+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
129+
sock.settimeout(timeout)
130+
try:
131+
sock.connect((self.ip, self.TCP_PORT))
132+
sent = sock.send(my_buf)
133+
except socket.timeout as e:
134+
raise Exception("Timeout while trying to send command {}".format(command)) from e
135+
136+
if len(my_buf) != sent:
137+
raise ConnectionError(
138+
"Failed sending entire buffer to projector. Sent {} out of {} !".format(sent, len(my_buf)))
139+
response_buf = sock.recv(1024)
140+
sock.close()
141+
142+
_, is_success, _, data = process_command_response(response_buf)
143+
144+
if not is_success:
145+
raise Exception(
146+
"Received failed status from projector while sending command 0x{:x}. Error 0x{:x}".format(command,
147+
data))
148+
return data
149+
150+
def find_projector(self, udp_ip: str = None, udp_port: int = None, timeout=None):
151+
152+
self.UDP_PORT = udp_port if udp_port is not None else self.UDP_PORT
153+
self.UDP_IP = udp_ip if udp_ip is not None else self.UDP_IP
154+
timeout = timeout if timeout is not None else self.UDP_TIMEOUT
155+
156+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
157+
158+
sock.bind((self.UDP_IP, self.UDP_PORT))
159+
160+
sock.settimeout(timeout)
161+
try:
162+
SDAP_buffer, addr = sock.recvfrom(1028)
163+
except socket.timeout as e:
164+
return False
165+
166+
self.header, self.info = process_SDAP(SDAP_buffer)
167+
self.ip = addr[0]
168+
self.is_init = True
169+
170+
def set_power(self, on=True):
171+
self._send_command(action=ACTIONS["SET"], command=COMMANDS["SET_POWER"],
172+
data=POWER_STATUS["START_UP"] if on else POWER_STATUS["STANDBY"])
173+
return True
174+
175+
def set_HDMI_input(self, hdmi_num: int):
176+
self._send_command(action=ACTIONS["SET"], command=COMMANDS["INPUT"],
177+
data=INPUTS["HDMI1"] if hdmi_num == 1 else INPUTS["HDMI2"])
178+
return True
179+
180+
def get_power(self):
181+
data = self._send_command(action=ACTIONS["GET"], command=COMMANDS["GET_STATUS_POWER"])
182+
if data == POWER_STATUS["STANDBY"] or data == POWER_STATUS["COOLING"] or data == POWER_STATUS["COOLING2"]:
183+
return False
184+
else:
185+
return True
186+
187+
def get_power_string(self):
188+
data = self._send_command(action=ACTIONS["GET"], command=COMMANDS["GET_STATUS_POWER"])
189+
return list(POWER_STATUS.keys())[list(POWER_STATUS.values()).index(data)]
190+
191+
192+
if __name__ == '__main__':
193+
# b = Projector()
194+
# b.find_projector(timeout=1)
195+
# # print(b.get_power())
196+
# # b = Projector("10.0.0.139")
197+
# # #
198+
# print(b.get_power())
199+
# print(b.set_power(False))
200+
# # import time
201+
# # time.sleep(7)
202+
# print (b.set_HDMI_input(1))
203+
# # time.sleep(7)
204+
# # print (b.set_HDMI_input(2))
205+
pass
6.15 KB
Binary file not shown.
749 Bytes
Binary file not shown.

‎pysdcp/protocol.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
# Defines and protocol details from here: https://www.digis.ru/upload/iblock/f5a/VPL-VW320,%20VW520_ProtocolManual.pdf
3+
4+
ACTIONS = {
5+
"GET": 0x01,
6+
"SET": 0x00
7+
}
8+
9+
COMMANDS = {
10+
"SET_POWER": 0x0130,
11+
"CALIBRATION_PRESET": 0x0002,
12+
"ASPECT_RATIO": 0x0020,
13+
"INPUT": 0x0001,
14+
"GET_STATUS_ERROR": 0x0101,
15+
"GET_STATUS_POWER": 0x0102,
16+
"GET_STATUS_LAMP_TIMER": 0x0113
17+
}
18+
19+
INPUTS = {
20+
"HDMI1": 0x002,
21+
"HDMI2": 0x003,
22+
}
23+
24+
ASPECT_RATIOS = {
25+
"NORMAL": '0001',
26+
"V_STRETCH": '000B',
27+
"ZOOM_1_85": '000C',
28+
"ZOOM_2_35": '000D',
29+
"STRETCH": '000E',
30+
"SQUEEZE": '000F'
31+
}
32+
33+
POWER_STATUS = {
34+
"STANDBY": 0,
35+
"START_UP": 1,
36+
"START_UP_LAMP": 2,
37+
"POWER_ON": 3,
38+
"COOLING": 4,
39+
"COOLING2": 5
40+
}

‎sony-pjtalk2mqtt.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
import paho.mqtt.client as mqtt
3+
import pysdcp
4+
import time
5+
6+
7+
projector = pysdcp.Projector('beamer.rzl')
8+
9+
def on_connect(client, userdata, flags, rc):
10+
print ("connected")
11+
client.subscribe("/service/beamer/command")
12+
13+
def on_message(client, userdata, msg):
14+
payload = msg.payload
15+
print(payload)
16+
if(payload == b'ON' or payload == b'1'):
17+
projector.set_power(True)
18+
elif (payload == b'OFF' or payload == b'0'):
19+
projector.set_power(False)
20+
else:
21+
client.publish("/service/beamer/error", payload="unknown command. please pass 0, 1, ON or OFF", qos=0)
22+
23+
lastState = ""
24+
client = mqtt.Client()
25+
client.on_connect = on_connect
26+
client.on_message = on_message
27+
client.connect_async("127.0.0.1")
28+
client.loop_start()
29+
30+
while True:
31+
state = 'unknown'
32+
try:
33+
state = projector.get_power_string()
34+
except:
35+
pass
36+
37+
print (state)
38+
39+
if(state != lastState):
40+
client.publish("/service/beamer/state", payload=state, qos=0, retain=True)
41+
lastState = state
42+
43+
time.sleep(1)

0 commit comments

Comments
 (0)
Please sign in to comment.