Skip to content

Instantly share code, notes, and snippets.

@bradfitz
Last active July 23, 2018 15:28
Show Gist options
  • Save bradfitz/eb50b05dd0457bf1f72f8338711f2041 to your computer and use it in GitHub Desktop.
Save bradfitz/eb50b05dd0457bf1f72f8338711f2041 to your computer and use it in GitHub Desktop.
Sonos amp control
// turnAmp controls the mPower Mini unit in Barloga, which
// powers the rainbow Google Chord AMP hooked up to the speakers.
func turnAmp(on bool) bool {
c, err := ssh.Dial("tcp", "10.0.0.nnnn:22", &ssh.ClientConfig{
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Config: ssh.Config{
Ciphers: []string{"aes128-cbc"},
},
User: "ubnt",
Auth: []ssh.AuthMethod{ssh.Password("xxxxx")},
})
if err != nil {
log.Printf("error toggling AMP connecting via ssh: %v", err)
return false
}
defer c.Close()
sess, err := c.NewSession()
if err != nil {
log.Printf("error toggling AMP connecting making ssh.NewSession: %v", err)
return false
}
val := "0"
if on {
val = "1"
}
err = sess.Run("echo " + val + " > /proc/power/relay1")
if err != nil {
log.Printf("Error running echo %s > /proc/power/relay1: %v", val, err)
return false
}
return true
}
package sonos
// NewDeviceFromIP returns a new Device given its IP address.
// This assumes that you've given your Sonos devices static IP
// addresses on your network. A future constructor might provide
// accessors by name.
func NewDeviceFromIP(ip string) *Device {
return &Device{ip: ip}
}
// Device represents a specific Sonos Device, such as a ZonePlayer,
// AMP, or Connect.
type Device struct {
ip string
}
// Status reports the device's status. States include TRANSITIONING, STOPPED,
// PLAYING, PAUSED_PLAYBACK.
func (d *Device) Status(ctx context.Context) (string, error) {
ipPort := net.JoinHostPort(d.ip, "1400")
req, err := http.NewRequest("POST", "http://"+ipPort+"/MediaRenderer/AVTransport/Control", strings.NewReader(`
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>
<u:GetTransportInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID></u:GetTransportInfo>
</s:Body></s:Envelope>
`))
if err != nil {
return "", err
}
req.Header["SOAPACTION"] = []string{`"urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo"`}
req.Header.Set("Content-Type", "text/xml")
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req = req.WithContext(ctx)
res, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
return "", err
}
defer res.Body.Close()
// Response looks like:
if res.StatusCode != 200 {
return "", fmt.Errorf("unexpected status code from %v: %v", d.ip, res.Status)
}
if ct := res.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/xml") {
return "", fmt.Errorf("unexpected content-type %q from %v", ct, d.ip)
}
// <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:GetTransportInfoResponse xmlns:u="urn:\
schemas-upnp-org:service:AVTransport:1"><CurrentTransportState>PAUSED_PLAYBACK</CurrentTransportState><CurrentTransportStatus>OK</CurrentTransportStatus><CurrentSpeed>1</CurrentSpeed><\
/u:GetTransportInfoResponse></s:Body></s:Envelope>
type infoRes struct {
CurrentTransportState string `xml:"CurrentTransportState"`
}
type soapBody struct {
XMLName xml.Name `xml:"Body"`
GetTransportInfoResponse infoRes `xml:"GetTransportInfoResponse"`
}
type soapEnvelope struct {
XMLName xml.Name `xml:"Envelope"`
Body soapBody `xml:"Body"`
}
var env soapEnvelope
if err := xml.NewDecoder(res.Body).Decode(&env); err != nil {
return "", err
}
state := env.Body.GetTransportInfoResponse.CurrentTransportState
if state == "" {
return "", fmt.Errorf("no CurrentTransportState in response")
}
return state, nil
}
func manageBarlogaAmp() {
d := sonos.NewDeviceFromIP(devices.BarlogaSonos.IP())
const timeUntilOff = 5 * time.Minute
offIn := timeUntilOff
var ampState = ampUnknown
var lastSonos string
for {
sonosState, err := d.Status(context.Background())
if err != nil {
log.Print(err)
time.Sleep(5 * time.Second)
continue
}
if lastSonos != sonosState {
log.Printf("sonos state transitioned from %q => %q (amp = %v)", lastSonos, sonosState, ampState)
lastSonos = sonosState
}
switch sonosState {
case "PLAYING", "TRANSITIONING":
offIn = timeUntilOff
if ampState != ampOn && turnAmp(true) {
log.Printf("Turned amp on.")
ampState = ampOn
}
default:
offIn -= time.Second
if offIn < 0 && ampState != ampOff && turnAmp(false) {
log.Printf("Turned amp off.")
ampState = ampOff
}
if offIn >= 0 && (offIn < 10*time.Second || int(offIn.Seconds())%10 == 0) {
log.Printf("Status: sonos=%q, amp=%s, offIn=%v", sonosState, ampState, offIn)
}
}
time.Sleep(1 * time.Second)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment