#!/usr/bin/python3 
# -*- coding: utf-8 -*-
# 
# based on code from lrvick and LiquidCrystal 
# lrvic - https://github.com/lrvick/raspi-hd44780/blob/master/hd44780.py 
# LiquidCrystal - https://github.com/arduino/Arduino/blob/master/libraries/LiquidCrystal/LiquidCrystal.cpp 
# 
import socket
from time import sleep 
import datetime
import RPi.GPIO as GPIO # 导入GPIO库
import urllib.request
import json
import requests
import socket
import smtplib
from email.mime.text import MIMEText # 由于需要定期向特点邮箱发送ip地址，所以要导入此库。如果没有发送ip地址到特定邮箱的需求，可以不用导入。

'''
树莓派电子日历-python控制代码
worked by cyclin. 知乎&酷安同号，邮箱联系1738296705@qq.com

说一下大致思路：树莓派通过GPIO控制LCD1602显示屏，显示时间日期、本机ip地址和天气三个部分的内容，其中屏幕第一行为日期，第二行滚动播放本机ip地址和天气。
要实现LCD屏幕的显示功能，就需要使用控制GPIO引脚的库，这一部分的代码均来自互联网，本人未作更改。封装好的函数为lcd.message(txt)，只需要改变txt参数即可改变显示屏上的内容。
本机ip通过socket返回，主要用于远程ssh登陆。详见get_host_ip()函数。
天气数据的爬取通过urllib.requset实现，具体内容封装在get_tianjin_weather()中。该函数爬取中国天气网的天津市当日天气数据（以html文本格式保存），需要通过字符串处理截取天气状况、最高气温、最低气温三个数据。由于LCD1602只能显示英文，get_tianjin_weather()函数在处理数据的过程中需要调用translate()函数，后者通过有道词典在线版将中文翻译成英文（会有一些词翻译的不到位，能看懂就行）。get_tianjin_weather()返回一个字符串，包含天气与气温。
邮件模块是我最后加的，因为有远程登陆的需求，必须定时把ip地址发送到指定邮箱。该部分代码封装在emailsend()函数中。为了保护隐私，发件人的邮箱地址与密码已做特殊处理（但是可能导致整个脚本运行失败），收件人是我的QQ邮箱，这里可以改。

下面看代码，我会在一些地方加入注释以便阅读。
'''


GPIO.setwarnings(False) # 这个是对树莓派的设置，不过具体是干什么的我也不太懂。
# 整个class Adafruit_CharLCD算是控制显示屏的底层驱动了吧，不过大部分代码都不太理解，反正最后封装了lcd.message()函数，直接调用那个就行。
# 以及，这一部分的代码最好放在脚本开头。
class Adafruit_CharLCD: 
	# commands 
	LCD_CLEARDISPLAY = 0x01 
	LCD_RETURNHOME = 0x02 
	LCD_ENTRYMODESET = 0x04 
	LCD_DISPLAYCONTROL = 0x08 
	LCD_CURSORSHIFT = 0x10 	
	LCD_FUNCTIONSET = 0x20 	
	LCD_SETCGRAMADDR = 0x40 
	LCD_SETDDRAMADDR = 0x80 

	# flags for display entry mode 
	LCD_ENTRYRIGHT = 0x00 
	LCD_ENTRYLEFT = 0x02 
	LCD_ENTRYSHIFTINCREMENT = 0x01 
	LCD_ENTRYSHIFTDECREMENT = 0x00 

	# flags for display on/off control 
	LCD_DISPLAYON = 0x04 
	LCD_DISPLAYOFF = 0x00 
	LCD_CURSORON = 0x02 
	LCD_CURSOROFF = 0x00 
	LCD_BLINKON = 0x01 
	LCD_BLINKOFF = 0x00 

	# flags for display/cursor shift 
	LCD_DISPLAYMOVE = 0x08 
	LCD_CURSORMOVE = 0x00 
	LCD_MOVERIGHT = 0x04 
	LCD_MOVELEFT = 0x00 
	
	# flags for function set 
	LCD_8BITMODE = 0x10 
	LCD_4BITMODE = 0x00 
	LCD_2LINE = 0x08 
	LCD_1LINE = 0x00 
	LCD_5x10DOTS = 0x04 
	LCD_5x8DOTS = 0x00 
	def __init__(self, pin_rs=14, pin_e=15, pins_db=[17, 18, 27, 22], GPIO = None): 
		# Emulate the old behavior of using RPi.GPIO if we haven't been given 
		# an explicit GPIO interface to use 
		if not GPIO: 
			import RPi.GPIO as GPIO 
		self.GPIO = GPIO 
		self.pin_rs = pin_rs 
		self.pin_e = pin_e 
		self.pins_db = pins_db 
		
		self.GPIO.setmode(GPIO.BCM) 
		self.GPIO.setup(self.pin_e, GPIO.OUT) 
		self.GPIO.setup(self.pin_rs, GPIO.OUT) 
		
		for pin in self.pins_db: 
			self.GPIO.setup(pin, GPIO.OUT) 
			
		self.write4bits(0x33) # initialization 
		self.write4bits(0x32) # initialization 
		self.write4bits(0x28) # 2 line 5x7 matrix 
		self.write4bits(0x0C) # turn cursor off 0x0E to enable cursor 
		self.write4bits(0x06) # shift cursor right 
		
		self.displaycontrol = self.LCD_DISPLAYON | self.LCD_CURSOROFF | self.LCD_BLINKOFF 
		self.displayfunction = self.LCD_4BITMODE | self.LCD_1LINE | self.LCD_5x8DOTS 
		self.displayfunction |= self.LCD_2LINE 
		
		""" Initialize to default text direction (for romance languages) """ 
		
		self.displaymode = self.LCD_ENTRYLEFT | self.LCD_ENTRYSHIFTDECREMENT 
		self.write4bits(self.LCD_ENTRYMODESET | self.displaymode) # set the entry mode 
		
		self.clear() 
	def begin(self, cols, lines): 
		if (lines > 1): 
			self.numlines = lines 
			self.displayfunction |= self.LCD_2LINE 
			self.currline = 0 
			
	def home(self): 
		self.write4bits(self.LCD_RETURNHOME) # set cursor position to zero 
		self.delayMicroseconds(3000) # this command takes a long time! 
		
	def clear(self): 
		self.write4bits(self.LCD_CLEARDISPLAY) # command to clear display 
		self.delayMicroseconds(3000) # 3000 microsecond sleep, clearing the display takes a long time 
		
	def setCursor(self, col, row): 
		self.row_offsets = [ 0x00, 0x40, 0x14, 0x54 ] 
		if ( row > self.numlines ): 
			row = self.numlines - 1 # we count rows starting w/0 
		
		self.write4bits(self.LCD_SETDDRAMADDR | (col + self.row_offsets[row])) 
		
	def noDisplay(self): 
		""" Turn the display off (quickly) """ 
		self.displaycontrol &= ~self.LCD_DISPLAYON 
		self.write4bits(self.LCD_DISPLAYCONTROL | self.displaycontrol) 
		
	def display(self): 
		""" Turn the display on (quickly) """ 
		self.displaycontrol |= self.LCD_DISPLAYON 
		self.write4bits(self.LCD_DISPLAYCONTROL | self.displaycontrol) 
		
	def noCursor(self): 
		""" Turns the underline cursor on/off """ 
		self.displaycontrol &= ~self.LCD_CURSORON 
		self.write4bits(self.LCD_DISPLAYCONTROL | self.displaycontrol) 
		
	def cursor(self): 
		""" Cursor On """ 
		self.displaycontrol |= self.LCD_CURSORON 
		self.write4bits(self.LCD_DISPLAYCONTROL | self.displaycontrol) 
		
	def noBlink(self): 
		""" Turn on and off the blinking cursor """ 
		self.displaycontrol &= ~self.LCD_BLINKON 
		self.write4bits(self.LCD_DISPLAYCONTROL | self.displaycontrol) 

		
	def DisplayLeft(self): 
		""" These commands scroll the display without changing the RAM """ 
		self.write4bits(self.LCD_CURSORSHIFT | self.LCD_DISPLAYMOVE | self.LCD_MOVELEFT) 
	
	def scrollDisplayRight(self): 
		""" These commands scroll the display without changing the RAM """ 
		self.write4bits(self.LCD_CURSORSHIFT | self.LCD_DISPLAYMOVE | self.LCD_MOVERIGHT); 
		
	def leftToRight(self): 
		""" This is for text that flows Left to Right """ 
		self.displaymode |= self.LCD_ENTRYLEFT 
		self.write4bits(self.LCD_ENTRYMODESET | self.displaymode); 
		
	def rightToLeft(self): 
		""" This is for text that flows Right to Left """ 
		self.displaymode &= ~self.LCD_ENTRYLEFT 
		self.write4bits(self.LCD_ENTRYMODESET | self.displaymode) 
		
	def autoscroll(self): 
		""" This will 'right justify' text from the cursor """ 
		self.displaymode |= self.LCD_ENTRYSHIFTINCREMENT 
		self.write4bits(self.LCD_ENTRYMODESET | self.displaymode) 
		
	def noAutoscroll(self): 
		""" This will 'left justify' text from the cursor """ 
		self.displaymode &= ~self.LCD_ENTRYSHIFTINCREMENT 
		self.write4bits(self.LCD_ENTRYMODESET | self.displaymode) 
		
	def write4bits(self, bits, char_mode=False): 
		""" Send command to LCD """ 
		self.delayMicroseconds(1000) # 1000 microsecond sleep 
		bits=bin(bits)[2:].zfill(8) 
		self.GPIO.output(self.pin_rs, char_mode) 
		for pin in self.pins_db: 
			self.GPIO.output(pin, False) 
		for i in range(4): 
			if bits[i] == "1": 
				self.GPIO.output(self.pins_db[::-1][i], True) 
		self.pulseEnable() 
		for pin in self.pins_db: 
			self.GPIO.output(pin, False) 
		for i in range(4,8): 
			if bits[i] == "1": 
				self.GPIO.output(self.pins_db[::-1][i-4], True) 
		self.pulseEnable() 
		
	def delayMicroseconds(self, microseconds): 
		seconds = microseconds / float(1000000) # divide microseconds by 1 million for seconds 
		sleep(seconds) 
		
	def pulseEnable(self): 
		self.GPIO.output(self.pin_e, False) 
		self.delayMicroseconds(1) # 1 microsecond pause - enable pulse must be > 450ns 
		self.GPIO.output(self.pin_e, True) 
		self.delayMicroseconds(1) # 1 microsecond pause - enable pulse must be > 450ns 
		self.GPIO.output(self.pin_e, False) 
		self.delayMicroseconds(1) # commands need > 37us to settle 
		
	def message(self, text): 
		""" Send string to LCD. Newline wraps to second line""" 
		for char in text: 
			if char == '\n': 
				self.write4bits(0xC0) # next line 
			else: 
				self.write4bits(ord(char),True) 	
# 底层驱动到这里结束。下面是我自定义的一些函数模块。

class tjweather: # 全局变量“天津天气”。方便在不同函数之间调用。
	text="N/A"

class last_ip: # 全局变量“设备的上一个ip地址”。方便在不同函数之间调用。由于校园网wlan是动态ip，必须在ip地址变动时通知到我。
	text='No Link'
				
def get_host_ip(): # 获取设备当前ip地址，如果设备离线，返回字符串"No Link"。
	""" check out my ip address :return:"""
	try:
		s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
		s.connect(('8.8.8.8',80))
		ip=s.getsockname()[0]
	except socket.error:ip='No Link'
	finally:
		s.close()
	return ip

def translate(word): # 调用有道词典网页版，对天气数据进行翻译。（直接爬到的是中文）	
	url = 'http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule&smartresult=ugc&sessionFrom=null' # use API of Youdao Dictionary	
	key = {
		'type': "AUTO",
		'i': word,
		"doctype": "json",
		"version": "2.1",
		"keyfrom": "fanyi.web",
		"ue": "UTF-8",
		"action": "FY_BY_CLICKBUTTON",
		"typoResult": "true"
	} # 翻译参数，其中变量i是要翻译的字符串。
	response = requests.post(url, data=key) # send "key" to Youdao server 
	x="NULL"
	try:
		if response.status_code == 200: # to judge the sending is successful or not
			x= response.text # then return the result
		else:
			x="link error"  # if failed, the return NULL
	except:x="link error" 
	return x


def get_reuslt(repsonse): # translate()返回的是一个json，这个函数将我们需要的字符串从json中提取出来。
	result = json.loads(repsonse)
	return result['translateResult'][0][0]['tgt']

def get_tianjin_weather(): # 获取天津天气
	try:
		ip=get_host_ip()
		if ip=='No Link':
			wth_fcst="Internet link error!" # 变量wth_fcst是此函数的返回值。如果设备离线，那么此函数直接返回"Internet link error!"，不会尝试链接到中国天气网。
		else:
			url="http://www.weather.com.cn/weather/101030100.shtml" # 中国天气网中天津市天气对应的页面。要想切换城市，将网址末尾的数字串101030100换成需要的城市对应页面即可。例如，北京市是101010100，上海市是101020100。
			req=urllib.request.Request(url)
			f_str=urllib.request.urlopen(req).read().decode('utf-8') # 解码
			f1=str(f_str)
			#下面是处理html文本的代码
			f_on=f1.find("fc_24h_internal_update_time") # 先找到这个关键词，当日天气在这个关键词附近
			f_off=f_on+435 # 这里确定了一个长度约为435的字符串，当日天气就包含在其中。
			f_today=f1[f_on:f_off] # 将这435个字切下来
			w1=f_today.find("wea")+5 # 今天天气情况对应字符串的起始位置
			w2=f_today.find("tem")-15 # 今天天气情况对应字符串的终止位置
			f_tdw=f_today[w1:w2] # 今天天气情况
			t1=f_today.find("tem")+7 # 今天温度情况对应字符串的起始位置
			t2=f_today.find("win")-20 # 今天温度情况对应字符串的终止位置
			f_tdt=f_today[t1:t2] # 今天气温情况（未处理）
			# 对今日气温情况进一步处理，得到今日最高气温与最低气温，或者是当前气温
			ctrl_1=f_tdt.find("</span>/<i>")
			if ctrl_1==-1:
				ctrl_1=f_tdt.find("i>")+2
				ctrl_2=f_tdt.find("</i>")
				temp=f_tdt[ctrl_1:ctrl_2]+ " C " #当前气温
			else:
				ctrl_2=ctrl_1+11
				tem_h=f_tdt[5:ctrl_1] #高温
				tem_l=f_tdt[ctrl_2:-1] #低温
				temp=tem_l+"/"+tem_h +" C "
			list_trans = translate(f_tdw) # 将天气情况翻译成英文，以便于屏幕显示。
			wth=get_reuslt(list_trans)+","
			wtf=wth.find("It's ") 
			if wtf==-1:
				weather=wth
			else:
				weather=wth[5:-1] # 将翻译结果的"It's "部分删除，以节省屏幕显示区域。
			wth_fcst=weather+" "+temp
	except:wth_fcst="Web Pages error!"
	return wth_fcst

def weatherrepo(ip_address): # 防止代码运行时出错，所以设计此函数，内容还是天气数据的处理。如果出错，则显示error而不是退出执行脚本。
	try:
		if ip_address=='No Link':
			txt='internet error! please check it!'
		else:
			txt=get_tianjin_weather()
			tjweather.text=txt
	except:txt="System error!"
	return txt

def emailsend(ip): # 发送邮件的函数。
	subject="ip address"
	now_time=datetime.datetime.now().strftime('%Y-%m-%d-%a---')+datetime.datetime.now().strftime('%H:%M:%S') # 获取当前日期与时间，作为邮件内容的一部分。
	content="<h1>ip address of my raspiberry:</h1><br><b>"+ip+"</b><br><br>current time: "+now_time+"<br>" # 这里是邮件内容

	sender="xxx@163.com" # 发件人，使用网易163邮箱。为了保护隐私，邮箱id已被xxx代替。
	password="xxx" # 邮箱密码。为了保护隐私，密码已被xxx代替。
	receivers=["1738296705@qq.com"] # 收件人，这是我的QQ邮箱
	txt="NULL" # 这个是要显示在LCD屏幕上的内容

	for receiver in receivers:
		message=MIMEText(content,"html","utf-8")
		message["From"]=sender
		message["To"]=receiver
		message["Subject"]=subject
        
		try:
			smtp=smtplib.SMTP_SSL('smtp.163.com',994)
			smtp.login(sender,password)
			smtp.sendmail(sender,receiver,message.as_string())
			smtp.close()
			txt="email sent...."
		except Exception:
			txt="email sent fail!"
		return txt

if __name__ == '__main__':  # 主函数
	lcd = Adafruit_CharLCD() 
	lcd.clear()
	ip_address=get_host_ip() # ip address

	#### 第一次启动先尝试发送ip地址的相关信息 ####
	words=emailsend(ip_address)
	lcd.message(words) 
	last_ip.text=ip_address


	#### 尝试获取今日天气 ###
	try:
		txt_wea=weatherrepo(ip_address)
	except:
		txt_wea="N/A"


	txt_info=" IP address:"+ip_address+"   Weather:"+txt_wea
	while True: # 无线循环执行下列代码
		lcd.home() # LCD屏幕指针指向左上角第一个位置
		now_time=datetime.datetime.now().strftime('%m-%d %w %H:%M:%S') # 屏幕第一行的显示内容，显示日期与时间，示例：08-22 6 13:53:28。格式为“月-日 星期 时：分：秒”

		#### 当现在时间的分钟部分为整十时，尝试获取天气，即天气每10分钟更新一次 ###
		if(now_time[12]=='0'):
			try:
				txt_wea=weatherrepo(ip_address)
			except:
				pass
			lcd.message("sync weather \ninformation...")
			lcd.home()


		ip_address=get_host_ip() # ip address

		##### 如果ip地址出现变动，则邮件告知使用者 ####
		if(ip_address==last_ip.text):
			pass
		else:
			words=emailsend(ip_address)
			lcd.message(words) 
			last_ip.text=ip_address


		txt_info=" IP address:"+ip_address+"    Weather:"+txt_wea+"  " # 屏幕第二行的滚动显示内容
		len_of_txtinfo=len(txt_info)
		# fuck python! fuck world!
		txt_sco=txt_info[0:(15 if(len_of_txtinfo>16) else len_of_txtinfo-1)] # 实现字幕滚动效果（从txt_info中截取长度为16的字符串，用于展示在第二行，同时每循环两次，这16个字符串的切片会向后移动一个字符，就像在滚动一样）
		#（别问我为什么设置成每循环两次滚动一个字符，因为为了提高时间显示的精度，我设置的是0.25秒刷新一次，但是这个刷新率太高，会导致字幕滚动太快，使用体验下降，所以设置成循环2次滚动一个字符，就是0.5秒滚动一个字符，这样使用体验会好一点）
		
		for i in range(0,len_of_txtinfo*2): # 循环，时间刷新与字幕滚动
			j=int(i/2)
			now_time=datetime.datetime.now().strftime('%m-%d %w %H:%M:%S')
			txt_show=now_time+"\n"+txt_sco
			lcd.message(txt_show)
			lcd.home() # 指针回到左上角
			sleep(0.25)
			# WCNM!
			txt_sco=txt_info[j:(j+15 if(len_of_txtinfo>(j+16)) else len_of_txtinfo-1)] # 切片后移一字符

			
