#!/bin/sh
# vaiopower - shell script for turning off and on various Sony Vaio laptop
# devices
#
# http://vaio-utils.org/power/
#
# This software is released under ISC License.
#
# Copyright © 2009–2011 David Jurenka <vaiopower.box NO-SP@M mail.jurenka.cz>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

set -e

VERSION="0.5.0"
USAGE="\
Usage: vaiopower <device> {on|off|toggle}
Supported devices include: (use any of the aliases from the right column)
 - Sound card       sound, audio
 - DVD drive        cd, dvd
 - Bluetooth        bt, bluetooth
 - WWAN adapter     wwan"
DEVICE="/sys/devices/platform/sony-laptop"
SLEEPFILE="/var/tmp/vaiopower-sleep.conf" # should survive hibernation & suspend
BTSTATEFILE="/var/tmp/vaiopower-bt-state.conf"

if [ "$1" = "-V" -o "$1" = "--version" ]; then
	echo "vaiopower - version $VERSION"
	exit 0
fi

if [ ! "$#" -eq 2 ] && [ "$1" != boot -a "$1" != dock -a "$1" != rfkill ]; then
	echo "$USAGE" >&2
	exit 1
fi

#convert input into lowercase
DEV=$(echo  "$1" | tr '[:upper:]' '[:lower:]')
MODE=$(echo "$2" | tr '[:upper:]' '[:lower:]')

case "$MODE" in
  up|on|1)
	MODE="up"
	;;
  down|off|0)
	MODE="down"
	;;
  toggle)
	;;
  "")
	;; # we may have just one argument
  *)
	echo "Error: Unrecognized mode!" >&2
	echo "$USAGE" >&2
	exit 1
	;;
esac

I=0
until [ -r "$DEVICE" ]; do
	I=$(expr $I + 1)
	if [ "$I" -eq 4 ]; then
		echo "Error: Cannot access directory $DEVICE." >&2
		echo "That means that either this programme is not being run on a Sony Vaio laptop," >&2
		echo "sysfs is not ready, or the kernel fails to load the sony_laptop kernel module." >&2
		exit 1
	fi
	sleep 1
done

if [ "$(id -u)" != 0 ]; then
	echo "Superuser privileges are required. Please run this with sudo." >&2
	exit 1
fi

umask 022 # for some reason this seems not to be the default when called by ACPI (how about udev?)

#-#set_audio_mute() {
#-#	[ "$1" = yes ] && PACTLARG=1 && AMIXERARG=mute
#-#	[ "$1" = no  ] && PACTLARG=0 && AMIXERARG=unmute
#-#
#-#	command -v pactl  >/dev/null && pactl set-sink-mute 0 $PACTLARG 2>/dev/null && return
#-#	command -v amixer >/dev/null &&	amixer -q set Master $AMIXERARG || true
#-#}

#-#get_audio_mute() {
#-#	# for detection of current mute status we use only amixer
#-#	# there seems to be no straightforward way to do this via PulseAudio as well as root can't access the server run by the user
#-#	command -v amixer >/dev/null &&	amixer get Master | grep -q off && return 0
#-#	return 1
#-#}

note_bt_state() {
	CONTENT="## Config file for vaiopower to preserve status of bluetooth ##"
	CONTENT="$CONTENT\n# Do not edit manually. This file is autogenerated when bluetooth is turned on/off"
	CONTENT="$CONTENT\nBTSTATE="$(cat $DEVICE/bluetoothpower)
	rm -f "$BTSTATEFILE" # could be a nasty symlink, delete it first
	printf "$CONTENT\n" > "$BTSTATEFILE"
}

audio_power_save() {
	[ -w /sys/module/snd_hda_intel/parameters/power_save ] && echo $1 > /sys/module/snd_hda_intel/parameters/power_save
}

case "$DEV" in
  audio|sound)
	if [ ! -w "$DEVICE/audiopower" ]; then
		echo "File $DEVICE/audiopower is not present in your system." >&2
		echo "Your laptop model does not allow for power management of this device." >&2
		exit 1
	fi
	if [ "$MODE" = down ]; then
		[ "$(cat $DEVICE/audiopower)" = 0 ] && echo "Audio is down already." >&2 && exit 1
		echo 0 > $DEVICE/audiopower
		#-#set_audio_mute yes
		# set audio powersaving timeout to 1 sec
		audio_power_save 1
		echo "Audio OFF"
	elif [ "$MODE" = up ]; then
		if [ "$(cat $DEVICE/audiopower)" = 1 ]; then
			#-#get_audio_mute && set_audio_mute no && echo "Audio ON" && exit
			echo "Audio is on already." >&2 && exit 1
		fi
		echo 1 > $DEVICE/audiopower
		#-#set_audio_mute no
		echo "Audio ON"
		#make sure that powersaving is on - this saves some power without degrading the performance
		audio_power_save 10
	else #toggle
		#-#if [ "$(cat $DEVICE/audiopower)" = 0 ] || get_audio_mute; then
		if [ "$(cat $DEVICE/audiopower)" = 0 ]; then
			$0 audio up
		else
			$0 audio down
		fi
	fi
	;;
  cd|dvd)
	if [ ! -w "$DEVICE/cdpower" ]; then
		echo "File $DEVICE/cdpower is not present in your system." >&2
		echo "Your laptop model does not allow for power management of this device." >&2
		exit 1
	fi
	if [ "$MODE" = down ]; then
		[ "$(cat $DEVICE/cdpower)" = 0 ] && echo "DVD is down already." >&2 && exit 1
		for SCSIDEVICE in /sys/class/scsi_device/*/device; do
			grep -q '^5$' $SCSIDEVICE/type && echo 1 > $SCSIDEVICE/delete && break
		done
		# Removing the device can take some time, and if we power it down before it's successfully removed,
		# the system will freeze. Hence the following sleep (2 sec at least).
		sleep 3
		echo 0 > $DEVICE/cdpower
		echo "DVD OFF"
	elif [ "$MODE" = up ]; then
		[ "$(cat $DEVICE/cdpower)" = 1 ] && echo "DVD is on already." >&2 && exit 1
		echo 1 > $DEVICE/cdpower
		#scan all devices as we don't know at which host the CD will be (caveat: this inevitably activates card reader too)
		for HOST in /sys/class/scsi_host/*/scan; do
			echo "- - -" > $HOST
		done
		echo "DVD ON"
	else #toggle
		if [ "$(cat $DEVICE/cdpower)" = 0 ]; then
			$0 cd up
		else
			$0 cd down
		fi
	fi
	;;
  bt|bluetooth)
	if [ ! -w "$DEVICE/bluetoothpower" ]; then
		echo "File $DEVICE/bluetoothpower is not present in your system." >&2
		echo "Your laptop model does not allow for power management of this device." >&2
		exit 1
	fi
	if [ "$MODE" = down ]; then
		# no testing of status here, see comment below
		##
		## no handling of user applets, they usually detect correctly that the systeme daemon gets killed or respawned
		## (at least GNOME's bluetooth-applet does)
		#-#killall bluetooth-applet || true
		if [ -x /etc/init.d/bluetooth ]; then # Debian
			/etc/init.d/bluetooth stop || true
		elif [ -x /etc/rc.d/bluetooth ]; then # Arch
			/etc/rc.d/bluetooth stop || true
		elif pgrep bluetoothd >/dev/null; then # fallback
			pkill -TERM bluetoothd || true
		fi
		sleep 2 #module is still busy here
		I=0
		until modprobe -r btusb; do #gives exit status 0, even if there's nothing to remove, so it's a good condition
			I=$(expr $I + 1)
			if [ "$I" -eq 5 ]; then
				echo "Unable to unload module btusb. Leaving it loaded." >&2
				break
			fi
			echo "Failed unloading module btusb. Repeating..." >&2
			sleep 2
		done
		#under some circumstances kernel reports bt as off despite LED being on and turning off has no effect, hence turn it on first
		echo 1 > $DEVICE/bluetoothpower
		echo 0 > $DEVICE/bluetoothpower
		echo "Bluetooth OFF"
	elif [ "$MODE" = up ]; then
		[ "$(cat $DEVICE/bluetoothpower)" = 1 ] && echo "Bluetooth is on already." >&2 && exit 1
		echo 1 > $DEVICE/bluetoothpower
		echo "Bluetooth ON"
		modprobe btusb
		if [ -x /etc/init.d/bluetooth ]; then # Debian
			/etc/init.d/bluetooth start || true
		elif [ -x /etc/rc.d/bluetooth ]; then # Arch
			/etc/rc.d/bluetooth start || true
		elif command -v bluetoothd >/dev/null; then # fallback
			bluetoothd || true
		fi
		###start bt-applet by regular user, not root
		#-#if [ ! -z "$SUDO_USER" ]; then
		#-#	sudo -u "$SUDO_USER" bluetooth-applet &
		#-#elif [ ! -z "$SUDO_UID" ]; then
		#-#	sudo -u "#$SUDO_UID" bluetooth-applet &
		#-#fi
	else #toggle
		if [ "$(cat $DEVICE/bluetoothpower)" = 0 ]; then
			$0 bt up
		else
			$0 bt down
		fi
	fi
	note_bt_state
	;;
  wwan)
	if [ ! -w "$DEVICE/wwanpower" ]; then
		echo "File $DEVICE/wwanpower is not present in your system." >&2
		echo "Your laptop model does not allow for power management of this device." >&2
		exit 1
	fi
	if [ "$MODE" = down ]; then
		[ "$(cat $DEVICE/wwanpower)" = 0 ] && echo "WWAN is down already." >&2 && exit 1
		echo 0 > $DEVICE/wwanpower
		echo "WWAN OFF"
	elif [ "$MODE" = up ]; then
		[ "$(cat $DEVICE/wwanpower)" = 1 ] && echo "WWAN is on already." >&2 && exit 1
		echo 1 > $DEVICE/wwanpower
		echo "WWAN ON"
	else #toggle
		if [ "$(cat $DEVICE/wwanpower)" = 0 ]; then
			$0 wwan up
		else
			$0 wwan down
		fi
	fi
	;;
  sleep)
	if [ "$MODE" = up ]; then # sleep on = suspending
		# maintain status of DVD, bluetooth, wwan, soundcard across hibernate/suspend
		CONTENT="## Config file for vaiopower to preserve power states across suspends ##"
		CONTENT="$CONTENT\n# Do not edit manually. This file is autogenerated during suspend"
		CONTENT="$CONTENT\n# and/or hibernate."

		# Save the power states
		[ -w $DEVICE/audiopower     ] && [ "$(cat $DEVICE/audiopower)"     = 0 ] && CONTENT="$CONTENT\nSOUNDCARDOFF=yes"
		[ -w $DEVICE/cdpower        ] && [ "$(cat $DEVICE/cdpower)"        = 0 ] && CONTENT="$CONTENT\nDVDOFF=yes"
		[ -w $DEVICE/bluetoothpower ] && [ "$(cat $DEVICE/bluetoothpower)" = 0 ] && CONTENT="$CONTENT\nBLUETOOTHOFF=yes"
		[ -w $DEVICE/wwanpower      ] && [ "$(cat $DEVICE/wwanpower)"      = 0 ] && CONTENT="$CONTENT\nWWANOFF=yes"

		rm -f "$SLEEPFILE" # could be a nasty symlink, delete first
		printf "$CONTENT\n" > "$SLEEPFILE"

		# System is unable to resume if it has been suspended with DVD off. Turn it on as a workaround.
		[ -w $DEVICE/cdpower ] && [ "$(cat $DEVICE/cdpower)" = 0 ] && echo 1 > $DEVICE/cdpower
	elif [ "$MODE" = down ]; then # sleep off = resuming
		if [ -r "$SLEEPFILE" ]; then
			. "$SLEEPFILE"
		else
			exit 1
		fi

		# CDROM SCSI device should not be present; just to be on the safe side before we power it down
		if [ "$DVDOFF" = yes ]; then
			for SCSIDEVICE in /sys/class/scsi_device/*/device; do
				grep -q '^5$' $SCSIDEVICE/type && echo 1 > $SCSIDEVICE/delete && break
			done
		fi

		[ "$BLUETOOTHOFF" = yes ] && echo 1 > $DEVICE/bluetoothpower && echo 0 > $DEVICE/bluetoothpower
		[ "$WWANOFF"      = yes ] && echo 0 > $DEVICE/wwanpower

		if [ "$SOUNDCARDOFF" = yes ]; then
			echo 0 > $DEVICE/audiopower
			audio_power_save 1
		else
			audio_power_save 10
		fi

		if [ "$DVDOFF" = yes ]; then
			sleep 2
			echo 0 > $DEVICE/cdpower
		fi
	fi
	;;
  rfkill)
	[ -r "$BTSTATEFILE" ] || exit 1
	BOOTTIME=$(grep ^btime /proc/stat | cut -f2 -d" ")
	MTIME=$(stat -c %Y "$BTSTATEFILE")
	[ "$MTIME" -lt "$BOOTTIME" ] && exit 1 # ignore setup file if it is a remnant from a previous session
	. "$BTSTATEFILE"
	[ "$BTSTATE" = 0 ] && echo 1 > $DEVICE/bluetoothpower && echo 0 > $DEVICE/bluetoothpower
	;;
  boot)
	if readlink -f $0 | grep -q /usr/local/; then
		BOOTCONFIG="/usr/local/etc/vaiopower.conf"
	else
		BOOTCONFIG="/etc/vaiopower.conf"
	fi
	if [ -r "$BOOTCONFIG" ]; then
		. "$BOOTCONFIG"
	else
		exit 1
	fi

	if [ "$DVD" = off ] && [ -w $DEVICE/cdpower ]; then
		for SCSIDEVICE in /sys/class/scsi_device/*/device; do
			grep -q '^5$' $SCSIDEVICE/type && echo 1 > $SCSIDEVICE/delete && CDDELETED=OK && break
		done
	fi

	if [ "$BLUETOOTH" = off ] && [ -w $DEVICE/bluetoothpower ]; then
		if [ -x /etc/init.d/bluetooth ]; then # Debian
			/etc/init.d/bluetooth stop || true
		elif [ -x /etc/rc.d/bluetooth ]; then # Arch
			/etc/rc.d/bluetooth stop || true
		elif pgrep bluetoothd >/dev/null; then # fallback
			pkill -TERM bluetoothd || true
		fi
		for I in $(seq 5); do
			modprobe -r btusb && break # gives exit status 0 even if there's nothing to remove, so it's a good condition
			sleep 1
		done
		echo 0 > $DEVICE/bluetoothpower # no need to turn it on first here
		note_bt_state
	fi

	if [ "$SOUNDCARD" = off ] && [ -w $DEVICE/audiopower ]; then
		echo 0 > $DEVICE/audiopower
		audio_power_save 1
		#-#set_audio_mute yes # This is futile here as when user logs into his/her session, the mute settings from the last session will be restored.
	else
		audio_power_save 10
	fi

	[ "$WWAN" = off ] && [ -w $DEVICE/wwanpower  ] && echo 0 > $DEVICE/wwanpower

	if [ "$CDDELETED" = OK ]; then
		sleep 2
		echo 0 > $DEVICE/cdpower
	fi
	;;
  dock)
	[ -w /sys/bus/pci/rescan ] || exit 1 # too old a kernel (< 2.6.30), not supported any more
	# Note: On VGN-S2 laptops (reported with S2HP and S2VP running Ubuntu 10.04) writing into
	# /sys/bus/pci/rescan results in keyboard and trackpad getting lost. However, it seems to be
	# the correct way (and probably also the only way) to do things now.
	# Therefore, check for model line and exit if we are on VGN-S2.
	if ! command -v dmidecode >/dev/null; then
		echo "Command \"dmidecode\" not found." >&2
		echo "Please install this programme to use this feature." >&2
		exit 1
	fi
	if dmidecode -s system-product-name | grep -q VGN-S2; then
		echo "The VGN-S2 laptops do not handle PCI bus rescans correctly." >&2
		echo "The redocking feature is therefore disabled on these laptops." >&2
		exit 1
	fi
	sleep 2 # give time to the kernel to detect the adapter
	echo 1 > /sys/bus/pci/rescan
	sleep 2 # give additional time to the kernel to detect the adapter
	for CARD in /sys/bus/pci/devices/*; do
		if lspci -s $(basename $CARD) | grep -q "Ethernet Controller"; then
			if [ "$(cat $CARD/net/eth*/operstate)" = "down" ]; then
				echo "Reinitializing $CARD"
				echo 1 > $CARD/remove
			fi
		fi
	done
	sleep 1
	echo 1 > /sys/bus/pci/rescan
	;;
  *)
	echo "Error: Unrecognized device!" >&2
	echo "$USAGE" >&2
	exit 1
	;;
esac

exit 0
