├── Demo.jpg ├── DemoPhoto ├── README.md ├── lib ├── Font.ttc ├── epd7in5.py ├── epd7in5_V2.py ├── epdconfig.py └── 字体.ttf ├── model ├── 1.9CM │ ├── Test_1_1.9.obj │ ├── Test_2_1.9.obj │ ├── Test_3_1.9.obj │ └── Test_4_1.9.Logo.obj ├── 2.5CM │ ├── Test_1_2.5.obj │ ├── Test_2_2.5.obj │ ├── Test_3_2.5.obj │ └── Test_4_2.5_Logo.obj └── Waveshare_2014.mb ├── o365_Test_Project ├── O365 │ ├── .vscode │ │ └── settings.json │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ ├── __version__.cpython-39.pyc │ │ ├── account.cpython-39.pyc │ │ ├── calendar.cpython-39.pyc │ │ ├── category.cpython-39.pyc │ │ ├── connection.cpython-39.pyc │ │ └── message.cpython-39.pyc │ ├── __version__.py │ ├── account.py │ ├── address_book.py │ ├── calendar.py │ ├── category.py │ ├── connection.py │ ├── directory.py │ ├── drive.py │ ├── excel.py │ ├── groups.py │ ├── mailbox.py │ ├── message.py │ ├── planner.py │ ├── sharepoint.py │ ├── tasks.py │ ├── teams.py │ └── utils │ │ ├── __init__.py │ │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ ├── attachment.cpython-39.pyc │ │ ├── consent.cpython-39.pyc │ │ ├── decorators.cpython-39.pyc │ │ ├── token.cpython-39.pyc │ │ ├── utils.cpython-39.pyc │ │ └── windows_tz.cpython-39.pyc │ │ ├── attachment.py │ │ ├── consent.py │ │ ├── decorators.py │ │ ├── token.py │ │ ├── utils.py │ │ └── windows_tz.py ├── PythonO365.py ├── PythonO365.pyproj └── PythonO365.sln ├── photo ├── Pla1.png ├── Pla2.png ├── Pla3.png ├── Pla4.png ├── demo0.jpg ├── demo1.jpg ├── demo2.jpg ├── demo3.jpg ├── demo4.jpg ├── demo5.jpg ├── demo6.jpg └── demo7.jpg ├── pic ├── bg.png ├── icon.png ├── moisture.png ├── temp.png └── weatherType │ ├── 中雨.bmp │ ├── 冻雨.bmp │ ├── 多云.bmp │ ├── 大雨.bmp │ ├── 小到中雨.bmp │ ├── 小雨.bmp │ ├── 无天气类型.bmp │ ├── 晴.bmp │ ├── 暴雨.bmp │ ├── 沙尘暴.bmp │ ├── 阴.bmp │ ├── 阵雨.bmp │ ├── 阵雪.bmp │ ├── 雨夹雪.bmp │ ├── 雷阵雨.bmp │ ├── 雷阵雨伴有冰雹.bmp │ └── 雾.bmp ├── scripts ├── WeatherIcon.json ├── WeatherStation.py ├── city_code.json └── 天气模板.json ├── startWeather.sh └── 设计界面.psd /Demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/Demo.jpg -------------------------------------------------------------------------------- /DemoPhoto: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Outlook墨水屏台历 WeatherStation by RaspberryPi 2 | ![image](https://github.com/ShaderFallback/RaspberryPi-WeatherStation/blob/master/photo/demo0.jpg) 3 | ![image](https://github.com/ShaderFallback/RaspberryPi-WeatherStation/blob/master/photo/demo1.jpg) 4 | ![image](https://github.com/ShaderFallback/RaspberryPi-WeatherStation/blob/master/photo/demo2.jpg) 5 | 6 | ### Display Time, Weather Forecast, and RSS News 7 | 8 | - **By default, it displays RSS news. The news source can be modified in the code at line 316.** 9 | - **To switch back to the schedule, uncomment lines 446 and 447, and comment out lines 448 and 449.** 10 | 11 | ### Hardware List 12 | 13 | - **Raspberry Pi Zero W** 14 | - **Waveshare 7.5inch e-Paper Display (Black and White)** 15 | - **e-Paper Driver HAT** 16 | 17 | e-Paper Related Information: 18 | [http://www.waveshare.net/wiki/7.5inch_e-Paper_HAT](http://www.waveshare.net/wiki/7.5inch_e-Paper_HAT) 19 | 20 | ### Raspberry Pi 21 | 22 | - **It is recommended to use the official Raspbian OS** 23 | [https://www.raspberrypi.org/downloads/](https://www.raspberrypi.org/downloads/) 24 | - **Python 3.0 or above** 25 | - **Enable GPIO** 26 | - **Install sudo** 27 | 28 | ### Usage 29 | 30 | - **Extract the files to the home directory.** 31 | - **Modify the city in `scripts/WeatherStation.py` at line 24 with the code at the end of the URL.** 32 | 33 | ```python 34 | r = requests.get('http://t.weather.sojson.com/api/weather/city/101010100') 35 | ``` 36 | - **To find city codes, please refer to the `scripts/city_code.json` file.(this WeatherAPI only China City)** 37 | - **Edit the `rc.local` file and add the startup item before `Exit 0`.** 38 | ``` python 39 | su pi -c "exec /home/pi/WeatherStation/startWeather.sh" 40 | ``` 41 | - **Reboot the Raspberry Pi with sudo reboot** 42 | 43 | 44 | ### 3D Printed Case 45 | - **The model in the pictures was printed using an FDM printer. In northern winters, please adjust the PLA nozzle temperature appropriately to prevent extrusion difficulties.** 46 | - **The model is measured in centimeters (CM, scaled to 10 in Cura). Please pay attention to the print size.** 47 | - **There are two versions of the model with thicknesses of 1.9CM and 2.5CM. If the driver board is directly plugged in, use the 2.5CM thick version (refer to internal details in the photos).** 48 | - **The Model folder contains Maya source files (2014 and above). Modify them as needed.** 49 | - **The base is available in versions with and without logos, choose according to your preference.** 50 | 51 | ### Others 52 | - **The directory includes PSD files for easy reference and layout changes.** 53 | - **To display rulers in Photoshop, go to View > Rulers, then right-click on the ruler to change the unit to "pixels". This makes it easy to set coordinate positions when pulling out guidelines.** 54 | 55 | 56 | ### 显示时间天气预报和RSS 新闻 57 | - **默认显示RSS新闻,新闻源可以修改代码316 行** 58 | - **如果想改回日程,取消注释446,447 行,并注释448,449行** 59 | 60 | ### 硬件清单 61 | - **树莓派 ZeroW** 62 | - **微雪7.5inch e-Paper 墨水屏(黑白)** 63 | - **e-Paper Driver HAT 驱动板** 64 | 65 | 墨水屏相关资料 66 | [http://www.waveshare.net/wiki/7.5inch_e-Paper_HAT](http://www.waveshare.net/wiki/7.5inch_e-Paper_HAT) 67 | 68 | ### 树莓派 69 | 70 | - **建议使用 官方的 Raspbian 系统** 71 | [https://www.raspberrypi.org/downloads/](https://www.raspberrypi.org/downloads/) 72 | - **Python 3.0 以上** 73 | - **开启 gpio** 74 | - **安装 sudo** 75 | 76 | ### 使用 77 | - **解压文件到 home 目录下** 78 | - **修改城市 scripts/ WeatherStation.py 24行 网址末尾代码** 79 | 80 | ``` python 81 | r =requests.get('http://t.weather.sojson.com/api/weather/city/101010100') 82 | ``` 83 | - **查询城市代码 请查看 scripts/ city_code.json 文件** 84 | - **编辑 rc.local 文件 在 Exit 0 之前添加开机 启动项** 85 | 86 | ``` python 87 | su pi -c "exec /home/pi/WeatherStation/startWeather.sh" 88 | ``` 89 | - **重启树莓派 sudo reboot** 90 | 91 | ### 3D打印外壳 92 | - **图片中模型使用FDM 打印机,北方冬天请适当提升PLA喷嘴温度,防止挤出困难** 93 | - **模型制作的单位为厘米CM(在Cura中缩放为10) 请注意打印尺寸** 94 | - **模型分为两种厚度版,1.9CM厚/2.5CM厚 两个版本,驱动板直插的话需要使用2.5CM 厚的版本(内部细节请看photo中图片)** 95 | - **Model文件夹中已包含maya(2014及以上)模型源文件,如有特殊需求请自行修改** 96 | - **底座有Logo 版和无Logo版 可自行选择** 97 | 98 | ![image](https://github.com/ShaderFallback/RaspberryPi-WeatherStation/blob/master/photo/demo3.jpg) 99 | 100 | - **打印参数参考** 101 | ![image](https://github.com/ShaderFallback/RaspberryPi-WeatherStation/blob/master/photo/Pla1.png) 102 | ![image](https://github.com/ShaderFallback/RaspberryPi-WeatherStation/blob/master/photo/Pla2.png) 103 | ![image](https://github.com/ShaderFallback/RaspberryPi-WeatherStation/blob/master/photo/Pla3.png) 104 | ![image](https://github.com/ShaderFallback/RaspberryPi-WeatherStation/blob/master/photo/Pla4.png) 105 | 106 | ### 其他 107 | - **目录中包含PSD文件 方便参考改变布局, 108 | PS 显示标尺 视图 > 标尺 ,在标尺上右键切换单位为 “像素” 109 | 拉出参考线时就可以很容易确定 坐标位置** 110 | 111 | 112 | - **有任何问题欢迎给我留言 :)** 113 | -------------------------------------------------------------------------------- /lib/Font.ttc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/lib/Font.ttc -------------------------------------------------------------------------------- /lib/epd7in5.py: -------------------------------------------------------------------------------- 1 | # ***************************************************************************** 2 | # * | File : epd7in5.py 3 | # * | Author : Waveshare team 4 | # * | Function : Electronic paper driver 5 | # * | Info : 6 | # *---------------- 7 | # * | This version: V4.0 8 | # * | Date : 2019-06-20 9 | # # | Info : python3 demo 10 | # ----------------------------------------------------------------------------- 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documnetation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and//or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in 19 | # all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | # THE SOFTWARE. 28 | # 29 | 30 | 31 | import epdconfig 32 | 33 | # Display resolution 34 | EPD_WIDTH = 640 35 | EPD_HEIGHT = 384 36 | 37 | class EPD: 38 | def __init__(self): 39 | self.reset_pin = epdconfig.RST_PIN 40 | self.dc_pin = epdconfig.DC_PIN 41 | self.busy_pin = epdconfig.BUSY_PIN 42 | self.cs_pin = epdconfig.CS_PIN 43 | self.width = EPD_WIDTH 44 | self.height = EPD_HEIGHT 45 | 46 | # Hardware reset 47 | def reset(self): 48 | epdconfig.digital_write(self.reset_pin, 1) 49 | epdconfig.delay_ms(200) 50 | epdconfig.digital_write(self.reset_pin, 0) 51 | epdconfig.delay_ms(10) 52 | epdconfig.digital_write(self.reset_pin, 1) 53 | epdconfig.delay_ms(200) 54 | 55 | def send_command(self, command): 56 | epdconfig.digital_write(self.dc_pin, 0) 57 | epdconfig.digital_write(self.cs_pin, 0) 58 | epdconfig.spi_writebyte([command]) 59 | epdconfig.digital_write(self.cs_pin, 1) 60 | 61 | def send_data(self, data): 62 | epdconfig.digital_write(self.dc_pin, 1) 63 | epdconfig.digital_write(self.cs_pin, 0) 64 | epdconfig.spi_writebyte([data]) 65 | epdconfig.digital_write(self.cs_pin, 1) 66 | 67 | def ReadBusy(self): 68 | #print("e-Paper busy") 69 | while(epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy 70 | epdconfig.delay_ms(100) 71 | #print("e-Paper busy release") 72 | 73 | def init(self): 74 | if (epdconfig.module_init() != 0): 75 | return -1 76 | # EPD hardware init start 77 | self.reset() 78 | 79 | self.send_command(0x01) # POWER_SETTING 80 | self.send_data(0x37) 81 | self.send_data(0x00) 82 | 83 | self.send_command(0x00) # PANEL_SETTING 84 | self.send_data(0xCF) 85 | self.send_data(0x08) 86 | 87 | self.send_command(0x06) # BOOSTER_SOFT_START 88 | self.send_data(0xc7) 89 | self.send_data(0xcc) 90 | self.send_data(0x28) 91 | 92 | self.send_command(0x04) # POWER_ON 93 | self.ReadBusy() 94 | 95 | self.send_command(0x30) # PLL_CONTROL 96 | self.send_data(0x3c) 97 | 98 | self.send_command(0x41) # TEMPERATURE_CALIBRATION 99 | self.send_data(0x00) 100 | 101 | self.send_command(0x50) # VCOM_AND_DATA_INTERVAL_SETTING 102 | self.send_data(0x77) 103 | 104 | self.send_command(0x60) # TCON_SETTING 105 | self.send_data(0x22) 106 | 107 | self.send_command(0x61) # TCON_RESOLUTION 108 | self.send_data(EPD_WIDTH >> 8) #source 640 109 | self.send_data(EPD_WIDTH & 0xff) 110 | self.send_data(EPD_HEIGHT >> 8) #gate 384 111 | self.send_data(EPD_HEIGHT & 0xff) 112 | 113 | self.send_command(0x82) # VCM_DC_SETTING 114 | self.send_data(0x1E) # decide by LUT file 115 | 116 | self.send_command(0xe5) # FLASH MODE 117 | self.send_data(0x03) 118 | 119 | # EPD hardware init end 120 | return 0 121 | 122 | def getbuffer(self, image): 123 | buf = [0x00] * (self.width * self.height // 4) 124 | image_monocolor = image.convert('1') 125 | imwidth, imheight = image_monocolor.size 126 | pixels = image_monocolor.load() 127 | #print("imwidth = ", imwidth, "imheight = ", imheight) 128 | if(imwidth == self.width and imheight == self.height): 129 | for y in range(imheight): 130 | for x in range(imwidth): 131 | # Set the bits for the column of pixels at the current position. 132 | if pixels[x, y] < 64: # black 133 | buf[(x + y * self.width) // 4] &= ~(0xC0 >> (x % 4 * 2)) 134 | elif pixels[x, y] < 192: # convert gray to red 135 | buf[(x + y * self.width) // 4] &= ~(0xC0 >> (x % 4 * 2)) 136 | buf[(x + y * self.width) // 4] |= 0x40 >> (x % 4 * 2) 137 | else: # white 138 | buf[(x + y * self.width) // 4] |= 0xC0 >> (x % 4 * 2) 139 | elif(imwidth == self.height and imheight == self.width): 140 | for y in range(imheight): 141 | for x in range(imwidth): 142 | newx = y 143 | newy = self.height - x - 1 144 | if pixels[x, y] < 64: # black 145 | buf[(newx + newy*self.width) // 4] &= ~(0xC0 >> (y % 4 * 2)) 146 | elif pixels[x, y] < 192: # convert gray to red 147 | buf[(newx + newy*self.width) // 4] &= ~(0xC0 >> (y % 4 * 2)) 148 | buf[(newx + newy*self.width) // 4] |= 0x40 >> (y % 4 * 2) 149 | else: # white 150 | buf[(newx + newy*self.width) // 4] |= 0xC0 >> (y % 4 * 2) 151 | return buf 152 | 153 | def display(self, image): 154 | self.send_command(0x10) 155 | for i in range(0, self.width // 4 * self.height): 156 | temp1 = image[i] 157 | j = 0 158 | while (j < 4): 159 | if ((temp1 & 0xC0) == 0xC0): 160 | temp2 = 0x03 161 | elif ((temp1 & 0xC0) == 0x00): 162 | temp2 = 0x00 163 | else: 164 | temp2 = 0x04 165 | temp2 = (temp2 << 4) & 0xFF 166 | temp1 = (temp1 << 2) & 0xFF 167 | j += 1 168 | if((temp1 & 0xC0) == 0xC0): 169 | temp2 |= 0x03 170 | elif ((temp1 & 0xC0) == 0x00): 171 | temp2 |= 0x00 172 | else: 173 | temp2 |= 0x04 174 | temp1 = (temp1 << 2) & 0xFF 175 | self.send_data(temp2) 176 | j += 1 177 | 178 | self.send_command(0x12) 179 | epdconfig.delay_ms(100) 180 | self.ReadBusy() 181 | 182 | def Clear(self): 183 | self.send_command(0x10) 184 | for i in range(0, self.width // 4 * self.height): 185 | for j in range(0, 4): 186 | self.send_data(0x33) 187 | 188 | self.send_command(0x12) 189 | self.ReadBusy() 190 | 191 | def sleep(self): 192 | self.send_command(0x02) # POWER_OFF 193 | self.ReadBusy() 194 | 195 | self.send_command(0x07) # DEEP_SLEEP 196 | self.send_data(0XA5) 197 | 198 | epdconfig.delay_ms(2000) 199 | epdconfig.module_exit() 200 | ### END OF FILE ### 201 | 202 | -------------------------------------------------------------------------------- /lib/epd7in5_V2.py: -------------------------------------------------------------------------------- 1 | # ***************************************************************************** 2 | # * | File : epd7in5.py 3 | # * | Author : Waveshare team 4 | # * | Function : Electronic paper driver 5 | # * | Info : 6 | # *---------------- 7 | # * | This version: V4.0 8 | # * | Date : 2019-06-20 9 | # # | Info : python demo 10 | # ----------------------------------------------------------------------------- 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documnetation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in 19 | # all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | # THE SOFTWARE. 28 | # 29 | 30 | 31 | import logging 32 | from . import epdconfig 33 | 34 | # Display resolution 35 | EPD_WIDTH = 800 36 | EPD_HEIGHT = 480 37 | 38 | logger = logging.getLogger(__name__) 39 | 40 | class EPD: 41 | def __init__(self): 42 | self.reset_pin = epdconfig.RST_PIN 43 | self.dc_pin = epdconfig.DC_PIN 44 | self.busy_pin = epdconfig.BUSY_PIN 45 | self.cs_pin = epdconfig.CS_PIN 46 | self.width = EPD_WIDTH 47 | self.height = EPD_HEIGHT 48 | 49 | # Hardware reset 50 | def reset(self): 51 | epdconfig.digital_write(self.reset_pin, 1) 52 | epdconfig.delay_ms(20) 53 | epdconfig.digital_write(self.reset_pin, 0) 54 | epdconfig.delay_ms(2) 55 | epdconfig.digital_write(self.reset_pin, 1) 56 | epdconfig.delay_ms(20) 57 | 58 | def send_command(self, command): 59 | epdconfig.digital_write(self.dc_pin, 0) 60 | epdconfig.digital_write(self.cs_pin, 0) 61 | epdconfig.spi_writebyte([command]) 62 | epdconfig.digital_write(self.cs_pin, 1) 63 | 64 | def send_data(self, data): 65 | epdconfig.digital_write(self.dc_pin, 1) 66 | epdconfig.digital_write(self.cs_pin, 0) 67 | epdconfig.spi_writebyte([data]) 68 | epdconfig.digital_write(self.cs_pin, 1) 69 | 70 | def send_data2(self, data): 71 | epdconfig.digital_write(self.dc_pin, 1) 72 | epdconfig.digital_write(self.cs_pin, 0) 73 | epdconfig.SPI.writebytes2(data) 74 | epdconfig.digital_write(self.cs_pin, 1) 75 | 76 | def ReadBusy(self): 77 | logger.debug("e-Paper busy") 78 | self.send_command(0x71) 79 | busy = epdconfig.digital_read(self.busy_pin) 80 | while(busy == 0): 81 | self.send_command(0x71) 82 | busy = epdconfig.digital_read(self.busy_pin) 83 | epdconfig.delay_ms(20) 84 | logger.debug("e-Paper busy release") 85 | 86 | def init(self): 87 | if (epdconfig.module_init() != 0): 88 | return -1 89 | # EPD hardware init start 90 | self.reset() 91 | 92 | self.send_command(0x06) # btst 93 | self.send_data(0x17) 94 | self.send_data(0x17) 95 | self.send_data(0x28) # If an exception is displayed, try using 0x38 96 | self.send_data(0x17) 97 | 98 | self.send_command(0x01) #POWER SETTING 99 | self.send_data(0x07) 100 | self.send_data(0x07) #VGH=20V,VGL=-20V 101 | self.send_data(0x3f) #VDH=15V 102 | self.send_data(0x3f) #VDL=-15V 103 | 104 | self.send_command(0x04) #POWER ON 105 | epdconfig.delay_ms(100) 106 | self.ReadBusy() 107 | 108 | self.send_command(0X00) #PANNEL SETTING 109 | self.send_data(0x1F) #KW-3f KWR-2F BWROTP 0f BWOTP 1f 110 | 111 | self.send_command(0x61) #tres 112 | self.send_data(0x03) #source 800 113 | self.send_data(0x20) 114 | self.send_data(0x01) #gate 480 115 | self.send_data(0xE0) 116 | 117 | self.send_command(0X15) 118 | self.send_data(0x00) 119 | 120 | self.send_command(0X50) #VCOM AND DATA INTERVAL SETTING 121 | self.send_data(0x10) 122 | self.send_data(0x07) 123 | 124 | self.send_command(0X60) #TCON SETTING 125 | self.send_data(0x22) 126 | 127 | # EPD hardware init end 128 | return 0 129 | 130 | def init_fast(self): 131 | if (epdconfig.module_init() != 0): 132 | return -1 133 | # EPD hardware init start 134 | self.reset() 135 | 136 | self.send_command(0X00) #PANNEL SETTING 137 | self.send_data(0x1F) #KW-3f KWR-2F BWROTP 0f BWOTP 1f 138 | 139 | self.send_command(0X50) #VCOM AND DATA INTERVAL SETTING 140 | self.send_data(0x10) 141 | self.send_data(0x07) 142 | 143 | self.send_command(0x04) #POWER ON 144 | epdconfig.delay_ms(100) 145 | self.ReadBusy() #waiting for the electronic paper IC to release the idle signal 146 | 147 | #Enhanced display drive(Add 0x06 command) 148 | self.send_command(0x06) #Booster Soft Start 149 | self.send_data (0x27) 150 | self.send_data (0x27) 151 | self.send_data (0x18) 152 | self.send_data (0x17) 153 | 154 | self.send_command(0xE0) 155 | self.send_data(0x02) 156 | self.send_command(0xE5) 157 | self.send_data(0x5A) 158 | 159 | # EPD hardware init end 160 | return 0 161 | 162 | def init_part(self): 163 | if (epdconfig.module_init() != 0): 164 | return -1 165 | # EPD hardware init start 166 | self.reset() 167 | 168 | self.send_command(0X00) #PANNEL SETTING 169 | self.send_data(0x1F) #KW-3f KWR-2F BWROTP 0f BWOTP 1f 170 | 171 | self.send_command(0x04) #POWER ON 172 | epdconfig.delay_ms(100) 173 | self.ReadBusy() #waiting for the electronic paper IC to release the idle signal 174 | 175 | self.send_command(0xE0) 176 | self.send_data(0x02) 177 | self.send_command(0xE5) 178 | self.send_data(0x6E) 179 | 180 | # EPD hardware init end 181 | return 0 182 | 183 | def getbuffer(self, image): 184 | img = image 185 | imwidth, imheight = img.size 186 | if(imwidth == self.width and imheight == self.height): 187 | img = img.convert('1') 188 | elif(imwidth == self.height and imheight == self.width): 189 | # image has correct dimensions, but needs to be rotated 190 | img = img.rotate(90, expand=True).convert('1') 191 | else: 192 | logger.warning("Wrong image dimensions: must be " + str(self.width) + "x" + str(self.height)) 193 | # return a blank buffer 194 | return [0x00] * (int(self.width/8) * self.height) 195 | 196 | buf = bytearray(img.tobytes('raw')) 197 | # The bytes need to be inverted, because in the PIL world 0=black and 1=white, but 198 | # in the e-paper world 0=white and 1=black. 199 | for i in range(len(buf)): 200 | buf[i] ^= 0xFF 201 | return buf 202 | 203 | def display(self, image): 204 | if(self.width % 8 == 0): 205 | Width = self.width // 8 206 | else: 207 | Width = self.width // 8 +1 208 | Height = self.height 209 | image1 = [0xFF] * int(self.width * self.height / 8) 210 | for j in range(Height): 211 | for i in range(Width): 212 | image1[i + j * Width] = ~image[i + j * Width] 213 | self.send_command(0x10) 214 | self.send_data2(image1) 215 | 216 | self.send_command(0x13) 217 | self.send_data2(image) 218 | 219 | self.send_command(0x12) 220 | epdconfig.delay_ms(100) 221 | self.ReadBusy() 222 | 223 | def Clear(self): 224 | self.send_command(0x10) 225 | self.send_data2([0xFF] * int(self.width * self.height / 8)) 226 | self.send_command(0x13) 227 | self.send_data2([0x00] * int(self.width * self.height / 8)) 228 | 229 | self.send_command(0x12) 230 | epdconfig.delay_ms(100) 231 | self.ReadBusy() 232 | 233 | def display_Partial(self, Image, Xstart, Ystart, Xend, Yend): 234 | if((Xstart % 8 + Xend % 8 == 8 & Xstart % 8 > Xend % 8) | Xstart % 8 + Xend % 8 == 0 | (Xend - Xstart)%8 == 0): 235 | Xstart = Xstart // 8 * 8 236 | Xend = Xend // 8 * 8 237 | else: 238 | Xstart = Xstart // 8 * 8 239 | if Xend % 8 == 0: 240 | Xend = Xend // 8 * 8 241 | else: 242 | Xend = Xend // 8 * 8 + 1 243 | 244 | Width = (Xend - Xstart) // 8 245 | Height = Yend - Ystart 246 | 247 | self.send_command(0x50) 248 | self.send_data(0xA9) 249 | self.send_data(0x07) 250 | 251 | self.send_command(0x91) #This command makes the display enter partial mode 252 | self.send_command(0x90) #resolution setting 253 | self.send_data (Xstart//256) 254 | self.send_data (Xstart%256) #x-start 255 | 256 | self.send_data ((Xend-1)//256) 257 | self.send_data ((Xend-1)%256) #x-end 258 | 259 | self.send_data (Ystart//256) # 260 | self.send_data (Ystart%256) #y-start 261 | 262 | self.send_data ((Yend-1)//256) 263 | self.send_data ((Yend-1)%256) #y-end 264 | self.send_data (0x01) 265 | 266 | image1 = [0xFF] * int(self.width * self.height / 8) 267 | for j in range(Height): 268 | for i in range(Width): 269 | image1[i + j * Width] = ~Image[i + j * Width] 270 | 271 | self.send_command(0x13) #Write Black and White image to RAM 272 | self.send_data2(image1) 273 | 274 | self.send_command(0x12) 275 | epdconfig.delay_ms(100) 276 | self.ReadBusy() 277 | 278 | def sleep(self): 279 | self.send_command(0x02) # POWER_OFF 280 | self.ReadBusy() 281 | 282 | self.send_command(0x07) # DEEP_SLEEP 283 | self.send_data(0XA5) 284 | 285 | epdconfig.delay_ms(2000) 286 | epdconfig.module_exit() 287 | ### END OF FILE ### 288 | -------------------------------------------------------------------------------- /lib/epdconfig.py: -------------------------------------------------------------------------------- 1 | # /***************************************************************************** 2 | # * | File : epdconfig.py 3 | # * | Author : Waveshare team 4 | # * | Function : Hardware underlying interface 5 | # * | Info : 6 | # *---------------- 7 | # * | This version: V1.0 8 | # * | Date : 2019-06-21 9 | # * | Info : 10 | # ****************************************************************************** 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documnetation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in 19 | # all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | # THE SOFTWARE. 28 | # 29 | 30 | import os 31 | import logging 32 | import sys 33 | import time 34 | 35 | 36 | class RaspberryPi: 37 | # Pin definition 38 | RST_PIN = 17 39 | DC_PIN = 25 40 | CS_PIN = 8 41 | BUSY_PIN = 24 42 | 43 | def __init__(self): 44 | import spidev 45 | import RPi.GPIO 46 | 47 | self.GPIO = RPi.GPIO 48 | self.SPI = spidev.SpiDev() 49 | 50 | def digital_write(self, pin, value): 51 | self.GPIO.output(pin, value) 52 | 53 | def digital_read(self, pin): 54 | return self.GPIO.input(pin) 55 | 56 | def delay_ms(self, delaytime): 57 | time.sleep(delaytime / 1000.0) 58 | 59 | def spi_writebyte(self, data): 60 | self.SPI.writebytes(data) 61 | 62 | def spi_writebyte2(self, data): 63 | self.SPI.writebytes2(data) 64 | 65 | def module_init(self): 66 | self.GPIO.setmode(self.GPIO.BCM) 67 | self.GPIO.setwarnings(False) 68 | self.GPIO.setup(self.RST_PIN, self.GPIO.OUT) 69 | self.GPIO.setup(self.DC_PIN, self.GPIO.OUT) 70 | self.GPIO.setup(self.CS_PIN, self.GPIO.OUT) 71 | self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN) 72 | 73 | # SPI device, bus = 0, device = 0 74 | self.SPI.open(0, 0) 75 | self.SPI.max_speed_hz = 4000000 76 | self.SPI.mode = 0b00 77 | return 0 78 | 79 | def module_exit(self): 80 | logging.debug("spi end") 81 | self.SPI.close() 82 | 83 | logging.debug("close 5V, Module enters 0 power consumption ...") 84 | self.GPIO.output(self.RST_PIN, 0) 85 | self.GPIO.output(self.DC_PIN, 0) 86 | 87 | self.GPIO.cleanup() 88 | 89 | 90 | class JetsonNano: 91 | # Pin definition 92 | RST_PIN = 17 93 | DC_PIN = 25 94 | CS_PIN = 8 95 | BUSY_PIN = 24 96 | 97 | def __init__(self): 98 | import ctypes 99 | find_dirs = [ 100 | os.path.dirname(os.path.realpath(__file__)), 101 | '/usr/local/lib', 102 | '/usr/lib', 103 | ] 104 | self.SPI = None 105 | for find_dir in find_dirs: 106 | so_filename = os.path.join(find_dir, 'sysfs_software_spi.so') 107 | if os.path.exists(so_filename): 108 | self.SPI = ctypes.cdll.LoadLibrary(so_filename) 109 | break 110 | if self.SPI is None: 111 | raise RuntimeError('Cannot find sysfs_software_spi.so') 112 | 113 | import Jetson.GPIO 114 | self.GPIO = Jetson.GPIO 115 | 116 | def digital_write(self, pin, value): 117 | self.GPIO.output(pin, value) 118 | 119 | def digital_read(self, pin): 120 | return self.GPIO.input(self.BUSY_PIN) 121 | 122 | def delay_ms(self, delaytime): 123 | time.sleep(delaytime / 1000.0) 124 | 125 | def spi_writebyte(self, data): 126 | self.SPI.SYSFS_software_spi_transfer(data[0]) 127 | 128 | def module_init(self): 129 | self.GPIO.setmode(self.GPIO.BCM) 130 | self.GPIO.setwarnings(False) 131 | self.GPIO.setup(self.RST_PIN, self.GPIO.OUT) 132 | self.GPIO.setup(self.DC_PIN, self.GPIO.OUT) 133 | self.GPIO.setup(self.CS_PIN, self.GPIO.OUT) 134 | self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN) 135 | self.SPI.SYSFS_software_spi_begin() 136 | return 0 137 | 138 | def module_exit(self): 139 | logging.debug("spi end") 140 | self.SPI.SYSFS_software_spi_end() 141 | 142 | logging.debug("close 5V, Module enters 0 power consumption ...") 143 | self.GPIO.output(self.RST_PIN, 0) 144 | self.GPIO.output(self.DC_PIN, 0) 145 | 146 | self.GPIO.cleanup() 147 | 148 | 149 | if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'): 150 | implementation = RaspberryPi() 151 | else: 152 | implementation = JetsonNano() 153 | 154 | for func in [x for x in dir(implementation) if not x.startswith('_')]: 155 | setattr(sys.modules[__name__], func, getattr(implementation, func)) 156 | 157 | 158 | ### END OF FILE ### 159 | -------------------------------------------------------------------------------- /lib/字体.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/lib/字体.ttf -------------------------------------------------------------------------------- /model/1.9CM/Test_2_1.9.obj: -------------------------------------------------------------------------------- 1 | # This file uses centimeters as units for non-parametric coordinates. 2 | 3 | mtllib Test_2.mtl 4 | g default 5 | v 2.738411 -0.127975 -45.866508 6 | v 2.830122 -0.236772 -45.807957 7 | v 14.425535 -0.127975 -45.866508 8 | v 14.347656 -0.236772 -45.786858 9 | v 2.738411 -0.127975 -28.049591 10 | v 14.417417 -0.127975 -28.049591 11 | v 2.830114 -0.236772 -28.108150 12 | v 14.339597 -0.236772 -28.129314 13 | v 14.212112 1.317037 -45.678871 14 | v 2.907106 1.317037 -45.678871 15 | v 2.738411 1.320349 -45.866508 16 | v 14.425535 1.320349 -45.866508 17 | v 2.907106 1.317037 -28.237446 18 | v 14.204254 1.317037 -28.237446 19 | v 14.417417 1.320349 -28.049591 20 | v 2.738411 1.320349 -28.049591 21 | v 14.204254 -0.007295 -28.237446 22 | v 2.907106 -0.007295 -28.237446 23 | v 2.907106 -0.007295 -45.678871 24 | v 2.907106 0.690149 -37.266804 25 | v 2.907106 0.812814 -37.247375 26 | v 2.907106 0.923472 -37.190994 27 | v 2.907106 1.011291 -37.103172 28 | v 2.907106 1.067675 -36.992516 29 | v 2.907106 1.087103 -36.869850 30 | v 2.907106 1.067674 -36.747185 31 | v 2.907106 1.011291 -36.636528 32 | v 2.907106 0.923472 -36.548706 33 | v 2.907106 0.812814 -36.492325 34 | v 2.907106 0.690149 -36.472897 35 | v 2.907106 0.567483 -36.492325 36 | v 2.907106 0.456825 -36.548706 37 | v 2.907106 0.369006 -36.636528 38 | v 2.907106 0.312623 -36.747185 39 | v 2.907106 0.293194 -36.869850 40 | v 2.907106 0.312623 -36.992516 41 | v 2.907106 0.369006 -37.103172 42 | v 2.907106 0.456825 -37.190994 43 | v 2.907106 0.567483 -37.247375 44 | v 14.212112 -0.007295 -45.678871 45 | v 13.639137 -0.007295 -28.846914 46 | v 3.986983 -0.007295 -28.846914 47 | v 3.986983 -0.007295 -45.069405 48 | v 13.646173 -0.007295 -45.069405 49 | v 2.738411 0.456825 -37.190994 50 | v 2.738411 0.369006 -37.103172 51 | v 2.738411 0.312623 -36.992516 52 | v 2.738411 0.293194 -36.869850 53 | v 2.738411 0.312623 -36.747185 54 | v 2.738411 0.369006 -36.636528 55 | v 2.738411 0.456825 -36.548706 56 | v 2.738411 0.567483 -36.492325 57 | v 2.738411 0.690149 -36.472897 58 | v 2.738411 0.812814 -36.492325 59 | v 2.738411 0.923472 -36.548706 60 | v 2.738411 1.011291 -36.636528 61 | v 2.738411 1.067674 -36.747185 62 | v 2.738411 1.087103 -36.869850 63 | v 2.738411 1.067675 -36.992516 64 | v 2.738411 1.011291 -37.103172 65 | v 2.738411 0.923472 -37.190994 66 | v 2.738411 0.812814 -37.247375 67 | v 2.738411 0.690149 -37.266804 68 | v 2.738411 0.567483 -37.247375 69 | v 13.705485 -0.236773 -28.778944 70 | v 13.639137 -0.144991 -28.846914 71 | v 3.909589 -0.236773 -28.797491 72 | v 3.986983 -0.144991 -28.846914 73 | v 3.909584 -0.236773 -45.118816 74 | v 3.986983 -0.144991 -45.069405 75 | v 13.712568 -0.236773 -45.137310 76 | v 13.646173 -0.144991 -45.069405 77 | vt 0.000000 0.000000 78 | vt 1.000000 0.000000 79 | vt 1.000000 1.000000 80 | vt 0.000000 1.000000 81 | vt 0.000000 0.000000 82 | vt 1.000000 0.000000 83 | vt 1.000000 1.000000 84 | vt 0.000000 1.000000 85 | vt 0.000000 0.000000 86 | vt 1.000000 1.000000 87 | vt 0.000000 1.000000 88 | vt 1.000000 0.000000 89 | vt 1.000000 1.000000 90 | vt 0.000000 1.000000 91 | vt 0.000000 0.000000 92 | vt 1.000000 0.000000 93 | vt 1.000000 1.000000 94 | vt 0.000000 1.000000 95 | vt 0.000000 0.000000 96 | vt 1.000000 0.000000 97 | vt 1.000000 1.000000 98 | vt 0.000000 1.000000 99 | vt 0.000000 0.000000 100 | vt 1.000000 0.000000 101 | vt 1.000000 1.000000 102 | vt 0.000000 1.000000 103 | vt 0.000000 0.000000 104 | vt 1.000000 0.000000 105 | vt 1.000000 1.000000 106 | vt 0.000000 1.000000 107 | vt 0.010539 0.000000 108 | vt 0.989464 0.000000 109 | vt 1.000000 0.011738 110 | vt 0.000000 0.011733 111 | vt 1.000000 0.000000 112 | vt 0.989469 0.009283 113 | vt 0.010544 0.009283 114 | vt 0.014444 0.016413 115 | vt 0.981748 0.016413 116 | vt 1.000000 0.000000 117 | vt 0.000000 0.000000 118 | vt 0.018261 0.000000 119 | vt 0.985566 0.000000 120 | vt 1.000000 0.016382 121 | vt 0.000000 0.016382 122 | vt 1.000000 0.000000 123 | vt 0.000000 0.000000 124 | vt 0.000000 1.000000 125 | vt 1.000000 1.000000 126 | vt 0.000000 0.000000 127 | vt 0.006663 0.000000 128 | vt 0.992148 0.000000 129 | vt 0.000000 0.004473 130 | vt 0.000000 1.000000 131 | vt 0.000000 0.995528 132 | vt 0.006664 1.000000 133 | vt 1.000000 1.000000 134 | vt 0.992153 1.000000 135 | vt 1.000000 0.996714 136 | vt 1.000000 0.000000 137 | vt 1.000000 0.003287 138 | vt 0.516582 0.434013 139 | vt 0.513350 0.350455 140 | vt 0.508314 0.284144 141 | vt 0.501970 0.241569 142 | vt 0.494937 0.226898 143 | vt 0.487904 0.241569 144 | vt 0.481559 0.284144 145 | vt 0.476524 0.350455 146 | vt 0.473292 0.434013 147 | vt 0.472178 0.526638 148 | vt 0.473292 0.619262 149 | vt 0.476524 0.702820 150 | vt 0.481559 0.769132 151 | vt 0.487904 0.811707 152 | vt 0.494937 0.826377 153 | vt 0.501970 0.811707 154 | vt 0.508314 0.769132 155 | vt 0.513350 0.702820 156 | vt 0.516582 0.619262 157 | vt 0.517696 0.526638 158 | vt 1.000000 0.516239 159 | vt 1.000000 0.517329 160 | vt 1.000000 0.516239 161 | vt 1.000000 0.513074 162 | vt 1.000000 0.508145 163 | vt 1.000000 0.501934 164 | vt 1.000000 0.495050 165 | vt 1.000000 0.488165 166 | vt 1.000000 0.481954 167 | vt 1.000000 0.477025 168 | vt 1.000000 0.473861 169 | vt 1.000000 0.472770 170 | vt 1.000000 0.473861 171 | vt 1.000000 0.477025 172 | vt 1.000000 0.481954 173 | vt 1.000000 0.488165 174 | vt 1.000000 0.495050 175 | vt 1.000000 0.501934 176 | vt 1.000000 0.508145 177 | vt 1.000000 0.513074 178 | vt 0.612500 0.564175 179 | vt 0.625000 0.564175 180 | vt 0.625000 0.521816 181 | vt 0.612500 0.521816 182 | vt 0.600000 0.564175 183 | vt 0.612500 0.564175 184 | vt 0.612500 0.521816 185 | vt 0.600000 0.521816 186 | vt 0.587500 0.564175 187 | vt 0.600000 0.564175 188 | vt 0.600000 0.521816 189 | vt 0.587500 0.521816 190 | vt 0.575000 0.564175 191 | vt 0.587500 0.564175 192 | vt 0.587500 0.521816 193 | vt 0.575000 0.521816 194 | vt 0.562500 0.564175 195 | vt 0.575000 0.564175 196 | vt 0.575000 0.521816 197 | vt 0.562500 0.521816 198 | vt 0.550000 0.564175 199 | vt 0.562500 0.564175 200 | vt 0.562500 0.521816 201 | vt 0.550000 0.521816 202 | vt 0.537500 0.564175 203 | vt 0.550000 0.564175 204 | vt 0.550000 0.521816 205 | vt 0.537500 0.521816 206 | vt 0.525000 0.564175 207 | vt 0.537500 0.564175 208 | vt 0.537500 0.521816 209 | vt 0.525000 0.521816 210 | vt 0.512500 0.564175 211 | vt 0.525000 0.564175 212 | vt 0.525000 0.521816 213 | vt 0.512500 0.521816 214 | vt 0.500000 0.564175 215 | vt 0.512500 0.564175 216 | vt 0.512500 0.521816 217 | vt 0.500000 0.521816 218 | vt 0.487500 0.564175 219 | vt 0.500000 0.564175 220 | vt 0.500000 0.521816 221 | vt 0.487500 0.521816 222 | vt 0.475000 0.564175 223 | vt 0.487500 0.564175 224 | vt 0.487500 0.521816 225 | vt 0.475000 0.521816 226 | vt 0.462500 0.564175 227 | vt 0.475000 0.564175 228 | vt 0.475000 0.521816 229 | vt 0.462500 0.521816 230 | vt 0.450000 0.564175 231 | vt 0.462500 0.564175 232 | vt 0.462500 0.521816 233 | vt 0.450000 0.521816 234 | vt 0.437500 0.564175 235 | vt 0.450000 0.564175 236 | vt 0.450000 0.521816 237 | vt 0.437500 0.521816 238 | vt 0.425000 0.564175 239 | vt 0.437500 0.564175 240 | vt 0.437500 0.521816 241 | vt 0.425000 0.521816 242 | vt 0.412500 0.564175 243 | vt 0.425000 0.564175 244 | vt 0.425000 0.521816 245 | vt 0.412500 0.521816 246 | vt 0.412500 0.521816 247 | vt 0.400000 0.521816 248 | vt 0.400000 0.564175 249 | vt 0.412500 0.564175 250 | vt 0.400000 0.521816 251 | vt 0.387500 0.521816 252 | vt 0.387500 0.564175 253 | vt 0.400000 0.564175 254 | vt 0.375000 0.564175 255 | vt 0.387500 0.564175 256 | vt 0.387500 0.521816 257 | vt 0.375000 0.521816 258 | vt 0.000000 1.000000 259 | vt 1.000000 0.399960 260 | vt 1.000000 1.000000 261 | vt 0.000000 0.000000 262 | vt 0.000000 0.399960 263 | vt 1.000000 1.000000 264 | vt 1.000000 0.000000 265 | vt 1.000000 0.000000 266 | vt 0.000000 0.399960 267 | vt 0.000000 1.000000 268 | vt 1.000000 1.000000 269 | vt 1.000000 1.000000 270 | vt 0.000000 1.000000 271 | vt 1.000000 1.000000 272 | vt 0.000000 1.000000 273 | vt 0.000000 1.000000 274 | vt 0.000000 0.399887 275 | vt 1.000000 0.000000 276 | vt 0.000000 0.000000 277 | vt 1.000000 0.399960 278 | vt 0.000000 0.000000 279 | vt 1.000000 0.399960 280 | vt 1.000000 0.000000 281 | vt 0.000000 0.399920 282 | vt 0.000000 0.000000 283 | vn -0.000000 0.473902 0.880578 284 | vn -0.000008 0.474593 0.880205 285 | vn -0.001464 0.590345 0.807150 286 | vn -0.001478 0.591404 0.806374 287 | vn -0.000000 0.473949 -0.880552 288 | vn -0.001468 0.590693 -0.806895 289 | vn -0.000008 0.474643 -0.880179 290 | vn -0.001482 0.591755 -0.806117 291 | vn 0.764595 0.644511 0.000000 292 | vn 0.764595 0.644511 0.000000 293 | vn 0.764623 0.644478 0.000000 294 | vn 0.764623 0.644478 0.000000 295 | vn -0.813224 0.581950 -0.000371 296 | vn -0.813225 0.581950 -0.000371 297 | vn -0.813271 0.581885 -0.000371 298 | vn -0.813272 0.581884 -0.000371 299 | vn 0.000000 -0.999844 -0.017647 300 | vn 0.000000 -0.999844 -0.017647 301 | vn 0.000000 -0.999844 -0.017647 302 | vn 0.000000 -0.999844 -0.017647 303 | vn 0.000000 -0.999845 0.017626 304 | vn 0.000000 -0.999845 0.017626 305 | vn 0.000000 -0.999845 0.017626 306 | vn 0.000000 -0.999845 0.017626 307 | vn -0.019627 -0.999807 0.000000 308 | vn -0.019627 -0.999807 0.000000 309 | vn -0.019627 -0.999807 0.000000 310 | vn -0.019627 -0.999807 0.000000 311 | vn 0.015528 -0.999879 0.000007 312 | vn 0.015521 -0.999880 0.000007 313 | vn 0.015527 -0.999879 0.000007 314 | vn 0.015521 -0.999880 0.000007 315 | vn 0.000000 -0.000000 1.000000 316 | vn 0.000000 -0.000000 1.000000 317 | vn 0.000000 -0.000000 1.000000 318 | vn 0.000000 -0.000000 1.000000 319 | vn -1.000000 0.000000 0.000000 320 | vn -1.000000 -0.000000 -0.000000 321 | vn -1.000000 0.000000 -0.000000 322 | vn -1.000000 -0.000001 0.000000 323 | vn -1.000000 -0.000000 -0.000000 324 | vn -1.000000 0.000000 -0.000000 325 | vn -1.000000 0.000000 0.000000 326 | vn -1.000000 -0.000000 0.000000 327 | vn -1.000000 0.000000 0.000000 328 | vn -1.000000 -0.000000 0.000000 329 | vn -1.000000 0.000000 0.000000 330 | vn -1.000000 -0.000000 0.000000 331 | vn -1.000000 -0.000000 0.000000 332 | vn -1.000000 0.000000 0.000000 333 | vn -1.000000 0.000000 -0.000000 334 | vn -1.000000 -0.000000 0.000000 335 | vn -1.000000 -0.000000 0.000000 336 | vn -1.000000 -0.000000 0.000000 337 | vn -1.000000 -0.000000 0.000000 338 | vn -1.000000 -0.000000 -0.000000 339 | vn -1.000000 0.000000 0.000000 340 | vn -1.000000 0.000001 0.000000 341 | vn -1.000000 -0.000000 0.000000 342 | vn -1.000000 0.000000 0.000000 343 | vn 0.000000 0.000000 -1.000000 344 | vn 0.000000 0.000000 -1.000000 345 | vn 0.000000 0.000000 -1.000000 346 | vn 0.000000 0.000000 -1.000000 347 | vn 1.000000 0.000000 0.000451 348 | vn 1.000000 0.000000 0.000451 349 | vn 1.000000 0.000000 0.000451 350 | vn 1.000000 0.000000 0.000451 351 | vn 0.000000 -1.000000 0.000002 352 | vn 0.000000 -1.000000 0.000002 353 | vn 0.000000 -1.000000 0.000002 354 | vn 0.000000 -1.000000 0.000002 355 | vn -0.000001 -1.000000 0.000000 356 | vn -0.000001 -1.000000 0.000000 357 | vn -0.000001 -1.000000 0.000000 358 | vn -0.000001 -1.000000 0.000000 359 | vn 0.000000 -1.000000 -0.000002 360 | vn 0.000000 -1.000000 -0.000002 361 | vn 0.000000 -1.000000 -0.000002 362 | vn 0.000000 -1.000000 -0.000002 363 | vn 0.000002 -1.000000 0.000000 364 | vn 0.000002 -1.000000 0.000000 365 | vn 0.000002 -1.000000 0.000000 366 | vn 0.000002 -1.000000 0.000000 367 | vn 0.000000 -0.000000 1.000000 368 | vn 0.000000 -0.000000 1.000000 369 | vn 0.000000 -0.000000 1.000000 370 | vn 0.000000 -0.000000 1.000000 371 | vn -1.000000 -0.000000 0.000000 372 | vn -1.000000 -0.000000 0.000000 373 | vn -1.000000 -0.000000 0.000000 374 | vn -1.000000 -0.000000 0.000000 375 | vn 0.000000 0.000000 -1.000000 376 | vn 0.000000 0.000000 -1.000000 377 | vn 0.000000 0.000000 -1.000000 378 | vn 0.000000 0.000000 -1.000000 379 | vn 1.000000 0.000000 0.000434 380 | vn 1.000000 0.000000 0.000434 381 | vn 1.000000 0.000000 0.000434 382 | vn 1.000000 0.000000 0.000434 383 | vn -0.000000 1.000000 -0.000000 384 | vn 0.000000 1.000000 -0.000000 385 | vn 0.000000 1.000000 -0.000000 386 | vn -0.000000 1.000000 -0.000000 387 | vn 0.000000 1.000000 0.000000 388 | vn 0.000000 1.000000 0.000000 389 | vn -0.000000 1.000000 0.000000 390 | vn -0.000000 1.000000 0.000000 391 | vn 0.000000 0.000000 -1.000000 392 | vn 0.000000 0.000000 -1.000000 393 | vn 0.000000 0.000000 -1.000000 394 | vn 0.000000 0.000000 -1.000000 395 | vn 1.000000 0.000000 0.000000 396 | vn 1.000000 0.000000 0.000000 397 | vn 1.000000 0.000000 0.000000 398 | vn 1.000000 -0.000001 0.000000 399 | vn 1.000000 0.000000 0.000000 400 | vn 1.000000 0.000000 0.000000 401 | vn 1.000000 -0.000000 0.000000 402 | vn 1.000000 0.000000 0.000000 403 | vn 1.000000 -0.000000 0.000000 404 | vn 1.000000 -0.000000 0.000000 405 | vn 1.000000 0.000000 0.000000 406 | vn 1.000000 0.000000 0.000000 407 | vn 1.000000 -0.000000 0.000000 408 | vn 1.000000 -0.000000 0.000000 409 | vn 1.000000 0.000000 0.000000 410 | vn 1.000000 0.000000 0.000000 411 | vn 1.000000 0.000000 0.000000 412 | vn 1.000000 0.000000 0.000000 413 | vn 1.000000 0.000000 0.000000 414 | vn 1.000000 0.000000 0.000000 415 | vn 1.000000 -0.000000 0.000000 416 | vn 1.000000 0.000000 0.000000 417 | vn 1.000000 0.000001 0.000000 418 | vn 1.000000 -0.000000 0.000000 419 | vn 0.000000 -0.000000 1.000000 420 | vn 0.000000 -0.000000 1.000000 421 | vn 0.000000 -0.000000 1.000000 422 | vn 0.000000 -0.000000 1.000000 423 | vn -1.000000 -0.000000 -0.000456 424 | vn -1.000000 -0.000000 -0.000456 425 | vn -1.000000 -0.000000 -0.000456 426 | vn -1.000000 -0.000000 -0.000456 427 | vn -0.000000 1.000000 0.000001 428 | vn -0.000000 0.951056 -0.309019 429 | vn -0.000000 1.000000 0.000001 430 | vn -0.000000 0.951056 -0.309018 431 | vn -0.000000 0.951056 0.309019 432 | vn -0.000000 0.951056 0.309019 433 | vn -0.000000 0.809019 0.587782 434 | vn -0.000000 0.809019 0.587782 435 | vn -0.000000 0.587789 0.809015 436 | vn -0.000000 0.587788 0.809015 437 | vn -0.000000 0.309010 0.951059 438 | vn -0.000000 0.309009 0.951059 439 | vn 0.000000 -0.000000 1.000000 440 | vn 0.000000 -0.000000 1.000000 441 | vn 0.000000 -0.309009 0.951059 442 | vn 0.000000 -0.309010 0.951059 443 | vn 0.000000 -0.587788 0.809015 444 | vn 0.000000 -0.587788 0.809015 445 | vn 0.000000 -0.809019 0.587782 446 | vn 0.000000 -0.809019 0.587782 447 | vn 0.000000 -0.951056 0.309019 448 | vn 0.000000 -0.951056 0.309019 449 | vn 0.000000 -1.000000 -0.000000 450 | vn 0.000000 -1.000000 -0.000000 451 | vn 0.000000 -0.951056 -0.309019 452 | vn 0.000000 -0.951056 -0.309019 453 | vn 0.000000 -0.809019 -0.587782 454 | vn 0.000000 -0.809019 -0.587782 455 | vn 0.000000 -0.587788 -0.809015 456 | vn 0.000000 -0.587788 -0.809015 457 | vn 0.000000 -0.309009 -0.951059 458 | vn 0.000000 -0.309009 -0.951059 459 | vn -0.000000 0.000000 -1.000000 460 | vn 0.000000 -0.000000 -1.000000 461 | vn -0.000000 0.309009 -0.951059 462 | vn -0.000000 0.309009 -0.951059 463 | vn -0.000000 0.587788 -0.809015 464 | vn -0.000000 0.587788 -0.809015 465 | vn -0.000000 0.809019 -0.587782 466 | vn -0.000000 0.809019 -0.587783 467 | vn 0.810327 0.585978 0.000351 468 | vn 0.810327 0.585978 0.000351 469 | vn 0.810327 0.585978 0.000351 470 | vn 0.810327 0.585978 0.000351 471 | vn -0.000803 0.538805 0.842430 472 | vn -0.000803 0.538805 0.842430 473 | vn -0.000803 0.538805 0.842430 474 | vn -0.000803 0.538805 0.842430 475 | vn -0.764476 0.644652 0.000000 476 | vn -0.764476 0.644652 0.000000 477 | vn -0.764476 0.644652 0.000000 478 | vn -0.764476 0.644652 0.000000 479 | vn -0.000801 0.538556 -0.842590 480 | vn -0.000801 0.538556 -0.842590 481 | vn -0.000801 0.538556 -0.842590 482 | vn -0.000801 0.538556 -0.842590 483 | s 1 484 | f 1/57/1 2/58/2 3/54/3 485 | f 3/54/3 2/58/2 4/56/4 486 | s 2 487 | f 5/60/5 6/50/6 7/52/7 488 | f 7/52/7 6/50/6 8/51/8 489 | s 3 490 | f 2/59/9 1/57/10 7/61/11 491 | f 7/61/11 1/57/10 5/60/12 492 | s 4 493 | f 8/53/13 6/50/14 4/55/15 494 | f 4/55/15 6/50/14 3/54/16 495 | s 5 496 | f 10/43/17 11/44/18 9/42/19 497 | f 9/42/19 11/44/18 12/45/20 498 | s 6 499 | f 13/38/21 14/39/22 16/41/23 500 | f 16/41/23 14/39/22 15/40/24 501 | s 7 502 | f 16/41/25 11/35/26 13/37/27 503 | f 13/37/27 11/35/26 10/36/28 504 | s 8 505 | f 14/31/29 9/32/30 15/34/31 506 | f 15/34/31 9/32/30 12/33/32 507 | s 9 508 | f 17/27/33 14/30/34 18/28/35 509 | f 18/28/35 14/30/34 13/29/36 510 | s 10 511 | f 18/23/37 35/66/38 19/24/39 512 | f 35/66/38 36/65/40 19/24/39 513 | f 35/66/38 18/23/37 34/67/41 514 | f 36/65/40 37/64/42 19/24/39 515 | f 34/67/41 18/23/37 33/68/43 516 | f 37/64/42 38/63/44 19/24/39 517 | f 33/68/43 18/23/37 32/69/45 518 | f 38/63/44 39/62/46 19/24/39 519 | f 32/69/45 18/23/37 31/70/47 520 | f 39/62/46 20/81/48 19/24/39 521 | f 31/70/47 18/23/37 30/71/49 522 | f 18/23/37 13/26/50 30/71/49 523 | f 19/24/39 20/81/48 10/25/51 524 | f 20/81/48 21/80/52 10/25/51 525 | f 30/71/49 13/26/50 29/72/53 526 | f 21/80/52 22/79/54 10/25/51 527 | f 29/72/53 13/26/50 28/73/55 528 | f 22/79/54 23/78/56 10/25/51 529 | f 28/73/55 13/26/50 27/74/57 530 | f 23/78/56 24/77/58 10/25/51 531 | f 27/74/57 13/26/50 26/75/59 532 | f 24/77/58 25/76/60 10/25/51 533 | f 26/75/59 13/26/50 25/76/60 534 | f 25/76/60 13/26/50 10/25/51 535 | s 11 536 | f 40/19/61 19/20/62 9/22/63 537 | f 9/22/63 19/20/62 10/21/64 538 | s 12 539 | f 17/15/65 40/16/66 14/18/67 540 | f 14/18/67 40/16/66 9/17/68 541 | s 13 542 | f 41/1/69 17/14/70 42/12/71 543 | f 42/12/71 17/14/70 18/13/72 544 | s 14 545 | f 42/9/73 18/11/74 43/6/75 546 | f 43/6/75 18/11/74 19/10/76 547 | s 15 548 | f 44/5/77 43/6/78 40/8/79 549 | f 40/8/79 43/6/78 19/7/80 550 | s off 551 | f 17/4/81 41/1/82 44/2/83 40/3/84 552 | f 68/201/85 66/186/86 41/182/87 42/187/88 553 | f 70/203/89 68/190/90 42/191/91 43/195/92 554 | f 44/194/93 72/205/94 70/203/95 43/195/96 555 | f 41/182/97 66/198/98 72/183/99 44/184/100 556 | s 16 557 | f 65/200/101 67/188/102 7/189/103 8/185/104 558 | f 67/188/102 69/193/105 2/192/106 7/189/103 559 | f 4/197/107 2/192/106 69/193/105 71/196/108 560 | f 71/196/108 65/200/101 8/185/104 4/197/107 561 | s 17 562 | f 16/46/109 15/47/110 5/60/111 563 | f 5/60/111 15/47/110 6/50/112 564 | s 18 565 | f 11/49/113 16/46/114 58/88/115 566 | f 58/88/115 16/46/114 57/89/116 567 | f 57/89/116 16/46/114 56/90/117 568 | f 56/90/117 16/46/114 55/91/118 569 | f 55/91/118 16/46/114 54/92/119 570 | f 54/92/119 16/46/114 53/93/120 571 | f 16/46/114 5/60/121 53/93/120 572 | f 11/49/113 58/88/115 1/57/122 573 | f 58/88/115 59/87/123 1/57/122 574 | f 59/87/123 60/86/124 1/57/122 575 | f 60/86/124 61/85/125 1/57/122 576 | f 61/85/125 62/84/126 1/57/122 577 | f 62/84/126 63/83/127 1/57/122 578 | f 63/83/127 64/82/128 1/57/122 579 | f 53/93/120 5/60/121 52/94/129 580 | f 64/82/128 45/101/130 1/57/122 581 | f 52/94/129 5/60/121 51/95/131 582 | f 45/101/130 46/100/132 1/57/122 583 | f 51/95/131 5/60/121 50/96/133 584 | f 46/100/132 47/99/134 1/57/122 585 | f 50/96/133 5/60/121 49/97/135 586 | f 47/99/134 48/98/136 1/57/122 587 | f 49/97/135 5/60/121 48/98/136 588 | f 48/98/136 5/60/121 1/57/122 589 | s 19 590 | f 12/48/137 11/49/138 3/54/139 591 | f 3/54/139 11/49/138 1/57/140 592 | s 20 593 | f 15/47/141 12/48/142 6/50/143 594 | f 6/50/143 12/48/142 3/54/144 595 | s 21 596 | f 25/105/145 24/104/146 58/102/147 597 | f 58/102/147 24/104/146 59/103/148 598 | f 26/109/149 25/108/145 57/106/150 599 | f 57/106/150 25/108/145 58/107/147 600 | f 27/113/151 26/112/149 56/110/152 601 | f 56/110/152 26/112/149 57/111/150 602 | f 28/117/153 27/116/151 55/114/154 603 | f 55/114/154 27/116/151 56/115/152 604 | f 29/121/155 28/120/153 54/118/156 605 | f 54/118/156 28/120/153 55/119/154 606 | f 30/125/157 29/124/155 53/122/158 607 | f 53/122/158 29/124/155 54/123/156 608 | f 31/129/159 30/128/157 52/126/160 609 | f 52/126/160 30/128/157 53/127/158 610 | f 32/133/161 31/132/159 51/130/162 611 | f 51/130/162 31/132/159 52/131/160 612 | f 33/137/163 32/136/161 50/134/164 613 | f 50/134/164 32/136/161 51/135/162 614 | f 34/141/165 33/140/163 49/138/166 615 | f 49/138/166 33/140/163 50/139/164 616 | f 35/145/167 34/144/165 48/142/168 617 | f 48/142/168 34/144/165 49/143/166 618 | f 36/149/169 35/148/167 47/146/170 619 | f 47/146/170 35/148/167 48/147/168 620 | f 37/153/171 36/152/169 46/150/172 621 | f 46/150/172 36/152/169 47/151/170 622 | f 38/157/173 37/156/171 45/154/174 623 | f 45/154/174 37/156/171 46/155/172 624 | f 39/161/175 38/160/173 64/158/176 625 | f 64/158/176 38/160/173 45/159/174 626 | f 20/165/177 39/164/175 63/162/178 627 | f 63/162/178 39/164/175 64/163/176 628 | f 21/169/179 20/168/177 62/166/180 629 | f 62/166/180 20/168/177 63/167/178 630 | f 62/173/180 61/172/181 21/170/179 631 | f 21/170/179 61/172/181 22/171/182 632 | f 61/177/181 60/176/183 22/174/182 633 | f 22/174/182 60/176/183 23/175/184 634 | f 24/181/146 23/180/184 59/178/148 635 | f 59/178/148 23/180/184 60/179/183 636 | s off 637 | f 66/198/185 65/200/186 71/199/187 72/183/188 638 | f 65/200/189 66/186/190 68/201/191 67/188/192 639 | f 67/202/193 68/190/194 70/203/195 69/204/196 640 | f 69/204/197 70/203/198 72/205/199 71/206/200 641 | -------------------------------------------------------------------------------- /model/2.5CM/Test_2_2.5.obj: -------------------------------------------------------------------------------- 1 | # This file uses centimeters as units for non-parametric coordinates. 2 | 3 | mtllib Test_2_2.mtl 4 | g default 5 | v 22.433798 -0.161708 -45.712517 6 | v 22.525509 -0.270506 -45.653965 7 | v 34.120922 -0.161708 -45.712517 8 | v 34.043043 -0.270506 -45.632866 9 | v 22.433798 -0.161708 -27.895599 10 | v 34.112804 -0.161708 -27.895599 11 | v 22.525501 -0.270506 -27.954159 12 | v 34.034984 -0.270506 -27.975323 13 | v 33.907499 1.707922 -45.524879 14 | v 22.602493 1.707922 -45.524879 15 | v 22.433798 1.711233 -45.712517 16 | v 34.120922 1.711233 -45.712517 17 | v 22.602493 1.707922 -28.083454 18 | v 33.899641 1.707922 -28.083454 19 | v 34.112804 1.711233 -27.895599 20 | v 22.433798 1.711233 -27.895599 21 | v 33.899641 -0.041028 -28.083454 22 | v 22.602493 -0.041028 -28.083454 23 | v 22.602493 -0.041028 -45.524879 24 | v 22.602493 1.081033 -37.112812 25 | v 22.602493 1.203699 -37.093384 26 | v 22.602493 1.314357 -37.037003 27 | v 22.602493 1.402176 -36.949181 28 | v 22.602493 1.458559 -36.838524 29 | v 22.602493 1.477987 -36.715858 30 | v 22.602493 1.458559 -36.593193 31 | v 22.602493 1.402176 -36.482536 32 | v 22.602493 1.314357 -36.394714 33 | v 22.602493 1.203699 -36.338333 34 | v 22.602493 1.081033 -36.318905 35 | v 22.602493 0.958367 -36.338333 36 | v 22.602493 0.847709 -36.394714 37 | v 22.602493 0.759890 -36.482536 38 | v 22.602493 0.703507 -36.593193 39 | v 22.602493 0.684079 -36.715858 40 | v 22.602493 0.703507 -36.838524 41 | v 22.602493 0.759890 -36.949181 42 | v 22.602493 0.847709 -37.037003 43 | v 22.602493 0.958367 -37.093384 44 | v 33.907499 -0.041028 -45.524879 45 | v 33.334524 -0.041029 -28.692923 46 | v 23.682369 -0.041029 -28.692923 47 | v 23.682369 -0.041029 -44.915413 48 | v 33.341560 -0.041029 -44.915413 49 | v 22.433798 0.847709 -37.037003 50 | v 22.433798 0.759890 -36.949181 51 | v 22.433798 0.703507 -36.838524 52 | v 22.433798 0.684079 -36.715858 53 | v 22.433798 0.703507 -36.593193 54 | v 22.433798 0.759890 -36.482536 55 | v 22.433798 0.847709 -36.394714 56 | v 22.433798 0.958367 -36.338333 57 | v 22.433798 1.081033 -36.318905 58 | v 22.433798 1.203699 -36.338333 59 | v 22.433798 1.314357 -36.394714 60 | v 22.433798 1.402176 -36.482536 61 | v 22.433798 1.458559 -36.593193 62 | v 22.433798 1.477987 -36.715858 63 | v 22.433798 1.450433 -36.838524 64 | v 22.433798 1.402176 -36.949181 65 | v 22.433798 1.314357 -37.037003 66 | v 22.433798 1.203699 -37.093384 67 | v 22.433798 1.081033 -37.112812 68 | v 22.433798 0.958367 -37.093384 69 | v 33.400871 -0.270506 -28.624952 70 | v 33.334524 -0.178724 -28.692923 71 | v 23.604976 -0.270506 -28.643499 72 | v 23.682369 -0.178724 -28.692923 73 | v 23.604971 -0.270506 -44.964825 74 | v 23.682369 -0.178724 -44.915413 75 | v 33.407955 -0.270506 -44.983318 76 | v 33.341560 -0.178724 -44.915413 77 | vt 0.000000 0.000000 78 | vt 1.000000 0.000000 79 | vt 1.000000 1.000000 80 | vt 0.000000 1.000000 81 | vt 0.000000 0.000000 82 | vt 1.000000 0.000000 83 | vt 1.000000 1.000000 84 | vt 0.000000 1.000000 85 | vt 0.000000 0.000000 86 | vt 1.000000 1.000000 87 | vt 0.000000 1.000000 88 | vt 1.000000 0.000000 89 | vt 1.000000 1.000000 90 | vt 0.000000 1.000000 91 | vt 0.000000 0.000000 92 | vt 1.000000 0.000000 93 | vt 1.000000 1.000000 94 | vt 0.000000 1.000000 95 | vt 0.000000 0.000000 96 | vt 1.000000 0.000000 97 | vt 1.000000 1.000000 98 | vt 0.000000 1.000000 99 | vt 0.000000 0.000000 100 | vt 1.000000 0.000000 101 | vt 1.000000 1.000000 102 | vt 0.000000 1.000000 103 | vt 0.000000 0.000000 104 | vt 1.000000 0.000000 105 | vt 1.000000 1.000000 106 | vt 0.000000 1.000000 107 | vt 0.010539 0.000000 108 | vt 0.989464 0.000000 109 | vt 1.000000 0.011738 110 | vt 0.000000 0.011733 111 | vt 1.000000 0.000000 112 | vt 0.989469 0.009283 113 | vt 0.010544 0.009283 114 | vt 0.014444 0.016413 115 | vt 0.981748 0.016413 116 | vt 1.000000 0.000000 117 | vt 0.000000 0.000000 118 | vt 0.018261 0.000000 119 | vt 0.985566 0.000000 120 | vt 1.000000 0.016382 121 | vt 0.000000 0.016382 122 | vt 1.000000 0.000000 123 | vt 0.000000 0.000000 124 | vt 0.000000 1.000000 125 | vt 1.000000 1.000000 126 | vt 0.000000 0.000000 127 | vt 0.006663 0.000000 128 | vt 0.992148 0.000000 129 | vt 0.000000 0.004473 130 | vt 0.000000 1.000000 131 | vt 0.000000 0.995528 132 | vt 0.006664 1.000000 133 | vt 1.000000 1.000000 134 | vt 0.992153 1.000000 135 | vt 1.000000 0.996714 136 | vt 1.000000 0.000000 137 | vt 1.000000 0.003287 138 | vt 0.516582 0.434013 139 | vt 0.513350 0.350455 140 | vt 0.508314 0.284144 141 | vt 0.501970 0.241569 142 | vt 0.494937 0.226898 143 | vt 0.487904 0.241569 144 | vt 0.481559 0.284144 145 | vt 0.476524 0.350455 146 | vt 0.473292 0.434013 147 | vt 0.472178 0.526638 148 | vt 0.473292 0.619262 149 | vt 0.476524 0.702820 150 | vt 0.481559 0.769132 151 | vt 0.487904 0.811707 152 | vt 0.494937 0.826377 153 | vt 0.501970 0.811707 154 | vt 0.508314 0.769132 155 | vt 0.513350 0.702820 156 | vt 0.516582 0.619262 157 | vt 0.517696 0.526638 158 | vt 1.000000 0.516239 159 | vt 1.000000 0.517329 160 | vt 1.000000 0.516239 161 | vt 1.000000 0.513074 162 | vt 1.000000 0.508145 163 | vt 1.000000 0.501934 164 | vt 1.000000 0.495050 165 | vt 1.000000 0.488165 166 | vt 1.000000 0.481954 167 | vt 1.000000 0.477025 168 | vt 1.000000 0.473861 169 | vt 1.000000 0.472770 170 | vt 1.000000 0.473861 171 | vt 1.000000 0.477025 172 | vt 1.000000 0.481954 173 | vt 1.000000 0.488165 174 | vt 1.000000 0.495050 175 | vt 1.000000 0.501934 176 | vt 1.000000 0.508145 177 | vt 1.000000 0.513074 178 | vt 0.612500 0.564175 179 | vt 0.625000 0.564175 180 | vt 0.625000 0.521816 181 | vt 0.612500 0.521816 182 | vt 0.600000 0.564175 183 | vt 0.612500 0.564175 184 | vt 0.612500 0.521816 185 | vt 0.600000 0.521816 186 | vt 0.587500 0.564175 187 | vt 0.600000 0.564175 188 | vt 0.600000 0.521816 189 | vt 0.587500 0.521816 190 | vt 0.575000 0.564175 191 | vt 0.587500 0.564175 192 | vt 0.587500 0.521816 193 | vt 0.575000 0.521816 194 | vt 0.562500 0.564175 195 | vt 0.575000 0.564175 196 | vt 0.575000 0.521816 197 | vt 0.562500 0.521816 198 | vt 0.550000 0.564175 199 | vt 0.562500 0.564175 200 | vt 0.562500 0.521816 201 | vt 0.550000 0.521816 202 | vt 0.537500 0.564175 203 | vt 0.550000 0.564175 204 | vt 0.550000 0.521816 205 | vt 0.537500 0.521816 206 | vt 0.525000 0.564175 207 | vt 0.537500 0.564175 208 | vt 0.537500 0.521816 209 | vt 0.525000 0.521816 210 | vt 0.512500 0.564175 211 | vt 0.525000 0.564175 212 | vt 0.525000 0.521816 213 | vt 0.512500 0.521816 214 | vt 0.500000 0.564175 215 | vt 0.512500 0.564175 216 | vt 0.512500 0.521816 217 | vt 0.500000 0.521816 218 | vt 0.487500 0.564175 219 | vt 0.500000 0.564175 220 | vt 0.500000 0.521816 221 | vt 0.487500 0.521816 222 | vt 0.475000 0.564175 223 | vt 0.487500 0.564175 224 | vt 0.487500 0.521816 225 | vt 0.475000 0.521816 226 | vt 0.462500 0.564175 227 | vt 0.475000 0.564175 228 | vt 0.475000 0.521816 229 | vt 0.462500 0.521816 230 | vt 0.450000 0.564175 231 | vt 0.462500 0.564175 232 | vt 0.462500 0.521816 233 | vt 0.450000 0.521816 234 | vt 0.437500 0.564175 235 | vt 0.450000 0.564175 236 | vt 0.450000 0.521816 237 | vt 0.437500 0.521816 238 | vt 0.425000 0.564175 239 | vt 0.437500 0.564175 240 | vt 0.437500 0.521816 241 | vt 0.425000 0.521816 242 | vt 0.412500 0.564175 243 | vt 0.425000 0.564175 244 | vt 0.425000 0.521816 245 | vt 0.412500 0.521816 246 | vt 0.412500 0.521816 247 | vt 0.400000 0.521816 248 | vt 0.400000 0.564175 249 | vt 0.412500 0.564175 250 | vt 0.400000 0.521816 251 | vt 0.387500 0.521816 252 | vt 0.387500 0.564175 253 | vt 0.400000 0.564175 254 | vt 0.375000 0.564175 255 | vt 0.387500 0.564175 256 | vt 0.387500 0.521816 257 | vt 0.375000 0.521816 258 | vt 0.000000 1.000000 259 | vt 1.000000 0.399960 260 | vt 1.000000 1.000000 261 | vt 0.000000 0.000000 262 | vt 0.000000 0.399960 263 | vt 1.000000 1.000000 264 | vt 1.000000 0.000000 265 | vt 1.000000 0.000000 266 | vt 0.000000 0.399960 267 | vt 0.000000 1.000000 268 | vt 1.000000 1.000000 269 | vt 1.000000 1.000000 270 | vt 0.000000 1.000000 271 | vt 1.000000 1.000000 272 | vt 0.000000 1.000000 273 | vt 0.000000 1.000000 274 | vt 0.000000 0.399887 275 | vt 1.000000 0.000000 276 | vt 0.000000 0.000000 277 | vt 1.000000 0.399960 278 | vt 0.000000 0.000000 279 | vt 1.000000 0.399960 280 | vt 1.000000 0.000000 281 | vt 0.000000 0.399920 282 | vt 0.000000 0.000000 283 | vn -0.000000 0.473901 0.880578 284 | vn -0.000008 0.474593 0.880205 285 | vn -0.001464 0.590344 0.807150 286 | vn -0.001478 0.591404 0.806374 287 | vn -0.000000 0.473949 -0.880552 288 | vn -0.001468 0.590694 -0.806895 289 | vn -0.000008 0.474643 -0.880179 290 | vn -0.001482 0.591755 -0.806116 291 | vn 0.764589 0.644518 0.000000 292 | vn 0.764589 0.644518 0.000000 293 | vn 0.764622 0.644479 0.000000 294 | vn 0.764622 0.644479 0.000000 295 | vn -0.813224 0.581950 -0.000371 296 | vn -0.813225 0.581950 -0.000371 297 | vn -0.813271 0.581885 -0.000371 298 | vn -0.813271 0.581885 -0.000371 299 | vn 0.000000 -0.999844 -0.017648 300 | vn 0.000000 -0.999844 -0.017648 301 | vn 0.000000 -0.999844 -0.017647 302 | vn 0.000000 -0.999844 -0.017647 303 | vn 0.000000 -0.999845 0.017625 304 | vn 0.000000 -0.999845 0.017628 305 | vn 0.000000 -0.999845 0.017625 306 | vn 0.000000 -0.999845 0.017628 307 | vn -0.019627 -0.999807 0.000000 308 | vn -0.019627 -0.999807 0.000000 309 | vn -0.019627 -0.999807 0.000000 310 | vn -0.019627 -0.999807 0.000000 311 | vn 0.015528 -0.999879 0.000007 312 | vn 0.015521 -0.999880 0.000007 313 | vn 0.015528 -0.999880 0.000007 314 | vn 0.015521 -0.999880 0.000007 315 | vn 0.000000 -0.000000 1.000000 316 | vn 0.000000 -0.000000 1.000000 317 | vn 0.000000 -0.000000 1.000000 318 | vn 0.000000 -0.000000 1.000000 319 | vn -1.000000 0.000000 0.000000 320 | vn -1.000000 0.000005 0.000000 321 | vn -1.000000 0.000000 0.000000 322 | vn -1.000000 -0.000000 0.000000 323 | vn -1.000000 -0.000010 0.000000 324 | vn -1.000000 -0.000000 0.000000 325 | vn -1.000000 0.000010 0.000000 326 | vn -1.000000 -0.000000 0.000000 327 | vn -1.000000 -0.000007 0.000000 328 | vn -1.000000 -0.000000 0.000000 329 | vn -1.000000 0.000004 0.000000 330 | vn -1.000000 -0.000000 0.000000 331 | vn -1.000000 0.000000 0.000000 332 | vn -1.000000 -0.000000 0.000000 333 | vn -1.000000 -0.000000 0.000000 334 | vn -1.000000 -0.000000 0.000000 335 | vn -1.000000 -0.000005 0.000000 336 | vn -1.000000 -0.000000 0.000000 337 | vn -1.000000 0.000008 0.000000 338 | vn -1.000000 -0.000000 0.000000 339 | vn -1.000000 -0.000012 0.000000 340 | vn -1.000000 -0.000000 0.000000 341 | vn -1.000000 0.000014 0.000000 342 | vn -1.000000 -0.000007 0.000000 343 | vn 0.000000 0.000000 -1.000000 344 | vn 0.000000 0.000000 -1.000000 345 | vn 0.000000 0.000000 -1.000000 346 | vn 0.000000 0.000000 -1.000000 347 | vn 1.000000 0.000000 0.000451 348 | vn 1.000000 0.000000 0.000451 349 | vn 1.000000 0.000000 0.000451 350 | vn 1.000000 0.000000 0.000451 351 | vn 0.000000 -1.000000 0.000002 352 | vn 0.000000 -1.000000 0.000002 353 | vn 0.000000 -1.000000 0.000002 354 | vn 0.000000 -1.000000 0.000002 355 | vn -0.000001 -1.000000 0.000000 356 | vn -0.000001 -1.000000 0.000000 357 | vn -0.000001 -1.000000 0.000000 358 | vn -0.000001 -1.000000 0.000000 359 | vn 0.000000 -1.000000 -0.000002 360 | vn 0.000000 -1.000000 -0.000002 361 | vn 0.000000 -1.000000 -0.000002 362 | vn 0.000000 -1.000000 -0.000002 363 | vn 0.000002 -1.000000 0.000000 364 | vn 0.000002 -1.000000 0.000000 365 | vn 0.000002 -1.000000 0.000000 366 | vn 0.000002 -1.000000 0.000000 367 | vn 0.000000 -0.000000 1.000000 368 | vn 0.000000 -0.000000 1.000000 369 | vn 0.000000 -0.000000 1.000000 370 | vn 0.000000 -0.000000 1.000000 371 | vn -1.000000 -0.000000 0.000000 372 | vn -1.000000 -0.000000 0.000000 373 | vn -1.000000 -0.000000 0.000000 374 | vn -1.000000 -0.000000 0.000000 375 | vn 0.000000 0.000000 -1.000000 376 | vn 0.000000 0.000000 -1.000000 377 | vn 0.000000 0.000000 -1.000000 378 | vn 0.000000 0.000000 -1.000000 379 | vn 1.000000 0.000000 0.000434 380 | vn 1.000000 0.000000 0.000434 381 | vn 1.000000 0.000000 0.000434 382 | vn 1.000000 0.000000 0.000434 383 | vn -0.000000 1.000000 -0.000000 384 | vn 0.000000 1.000000 -0.000000 385 | vn 0.000000 1.000000 -0.000000 386 | vn -0.000000 1.000000 -0.000000 387 | vn 0.000000 1.000000 0.000000 388 | vn 0.000000 1.000000 0.000000 389 | vn -0.000000 1.000000 0.000000 390 | vn -0.000000 1.000000 0.000000 391 | vn 0.000000 0.000000 -1.000000 392 | vn 0.000000 0.000000 -1.000000 393 | vn 0.000000 0.000000 -1.000000 394 | vn 0.000000 0.000000 -1.000000 395 | vn 1.000000 0.000000 0.000000 396 | vn 1.000000 0.000000 0.000000 397 | vn 1.000000 0.000006 0.000000 398 | vn 1.000000 -0.000035 0.000000 399 | vn 1.000000 -0.000003 0.000000 400 | vn 1.000000 -0.000010 0.000000 401 | vn 1.000000 0.000002 0.000000 402 | vn 1.000000 0.000000 0.000000 403 | vn 1.000000 -0.000000 0.000000 404 | vn 1.000000 -0.000000 0.000000 405 | vn 1.000000 0.000000 0.000000 406 | vn 1.000000 0.000017 0.000000 407 | vn 1.000000 0.000032 0.000000 408 | vn 1.000000 0.000000 0.000000 409 | vn 1.000000 0.000000 0.000000 410 | vn 1.000000 0.000000 0.000000 411 | vn 1.000000 -0.000002 0.000000 412 | vn 1.000000 -0.000022 0.000000 413 | vn 1.000000 0.000009 0.000000 414 | vn 1.000000 -0.000012 0.000000 415 | vn 1.000000 0.000003 0.000000 416 | vn 1.000000 0.000000 0.000000 417 | vn 1.000000 0.000026 0.000000 418 | vn 1.000000 -0.000002 0.000000 419 | vn 0.000000 -0.000000 1.000000 420 | vn 0.000000 -0.000000 1.000000 421 | vn 0.000000 -0.000000 1.000000 422 | vn 0.000000 -0.000000 1.000000 423 | vn -1.000000 -0.000000 -0.000456 424 | vn -1.000000 -0.000000 -0.000456 425 | vn -1.000000 -0.000000 -0.000456 426 | vn -1.000000 -0.000000 -0.000456 427 | vn -0.000000 1.000000 0.000001 428 | vn -0.031277 0.947024 -0.319636 429 | vn -0.014363 0.999700 -0.019832 430 | vn -0.032543 0.946228 -0.321861 431 | vn -0.000000 0.951055 0.309021 432 | vn -0.000000 0.951056 0.309019 433 | vn -0.000000 0.809018 0.587784 434 | vn -0.000000 0.809020 0.587781 435 | vn -0.000000 0.587783 0.809019 436 | vn -0.000000 0.587788 0.809015 437 | vn -0.000000 0.309009 0.951059 438 | vn -0.000000 0.309011 0.951059 439 | vn 0.000000 -0.000001 1.000000 440 | vn 0.000000 -0.000001 1.000000 441 | vn 0.000000 -0.309008 0.951059 442 | vn 0.000000 -0.309010 0.951059 443 | vn 0.000000 -0.587786 0.809017 444 | vn 0.000000 -0.587790 0.809013 445 | vn 0.000000 -0.809016 0.587786 446 | vn 0.000000 -0.809018 0.587783 447 | vn 0.000000 -0.951056 0.309020 448 | vn 0.000000 -0.951056 0.309018 449 | vn 0.000000 -1.000000 -0.000000 450 | vn 0.000000 -1.000000 0.000000 451 | vn 0.000000 -0.951055 -0.309022 452 | vn 0.000000 -0.951056 -0.309019 453 | vn 0.000000 -0.809018 -0.587785 454 | vn 0.000000 -0.809020 -0.587781 455 | vn 0.000000 -0.587783 -0.809019 456 | vn 0.000000 -0.587788 -0.809015 457 | vn 0.000000 -0.309009 -0.951059 458 | vn 0.000000 -0.309011 -0.951059 459 | vn -0.000000 0.000001 -1.000000 460 | vn -0.000000 0.000001 -1.000000 461 | vn -0.000000 0.309008 -0.951059 462 | vn -0.000000 0.309010 -0.951059 463 | vn -0.000000 0.587790 -0.809013 464 | vn -0.000000 0.587786 -0.809016 465 | vn -0.000000 0.824774 -0.565463 466 | vn -0.013252 0.815210 -0.579014 467 | vn 0.810327 0.585978 0.000351 468 | vn 0.810327 0.585978 0.000351 469 | vn 0.810327 0.585978 0.000351 470 | vn 0.810327 0.585978 0.000351 471 | vn -0.000803 0.538805 0.842430 472 | vn -0.000803 0.538805 0.842430 473 | vn -0.000803 0.538805 0.842430 474 | vn -0.000803 0.538805 0.842430 475 | vn -0.764478 0.644650 0.000000 476 | vn -0.764478 0.644650 0.000000 477 | vn -0.764478 0.644650 0.000000 478 | vn -0.764478 0.644650 0.000000 479 | vn -0.000800 0.538556 -0.842589 480 | vn -0.000800 0.538556 -0.842590 481 | vn -0.000800 0.538556 -0.842589 482 | vn -0.000800 0.538556 -0.842589 483 | s 1 484 | f 1/57/1 2/58/2 3/54/3 485 | f 3/54/3 2/58/2 4/56/4 486 | s 2 487 | f 5/60/5 6/50/6 7/52/7 488 | f 7/52/7 6/50/6 8/51/8 489 | s 3 490 | f 2/59/9 1/57/10 7/61/11 491 | f 7/61/11 1/57/10 5/60/12 492 | s 4 493 | f 8/53/13 6/50/14 4/55/15 494 | f 4/55/15 6/50/14 3/54/16 495 | s 5 496 | f 10/43/17 11/44/18 9/42/19 497 | f 9/42/19 11/44/18 12/45/20 498 | s 6 499 | f 13/38/21 14/39/22 16/41/23 500 | f 16/41/23 14/39/22 15/40/24 501 | s 7 502 | f 16/41/25 11/35/26 13/37/27 503 | f 13/37/27 11/35/26 10/36/28 504 | s 8 505 | f 14/31/29 9/32/30 15/34/31 506 | f 15/34/31 9/32/30 12/33/32 507 | s 9 508 | f 17/27/33 14/30/34 18/28/35 509 | f 18/28/35 14/30/34 13/29/36 510 | s 10 511 | f 18/23/37 35/66/38 19/24/39 512 | f 35/66/38 36/65/40 19/24/39 513 | f 35/66/38 18/23/37 34/67/41 514 | f 36/65/40 37/64/42 19/24/39 515 | f 34/67/41 18/23/37 33/68/43 516 | f 37/64/42 38/63/44 19/24/39 517 | f 33/68/43 18/23/37 32/69/45 518 | f 38/63/44 39/62/46 19/24/39 519 | f 32/69/45 18/23/37 31/70/47 520 | f 39/62/46 20/81/48 19/24/39 521 | f 31/70/47 18/23/37 30/71/49 522 | f 18/23/37 13/26/50 30/71/49 523 | f 19/24/39 20/81/48 10/25/51 524 | f 20/81/48 21/80/52 10/25/51 525 | f 30/71/49 13/26/50 29/72/53 526 | f 21/80/52 22/79/54 10/25/51 527 | f 29/72/53 13/26/50 28/73/55 528 | f 22/79/54 23/78/56 10/25/51 529 | f 28/73/55 13/26/50 27/74/57 530 | f 23/78/56 24/77/58 10/25/51 531 | f 27/74/57 13/26/50 26/75/59 532 | f 24/77/58 25/76/60 10/25/51 533 | f 26/75/59 13/26/50 25/76/60 534 | f 25/76/60 13/26/50 10/25/51 535 | s 11 536 | f 40/19/61 19/20/62 9/22/63 537 | f 9/22/63 19/20/62 10/21/64 538 | s 12 539 | f 17/15/65 40/16/66 14/18/67 540 | f 14/18/67 40/16/66 9/17/68 541 | s 13 542 | f 41/1/69 17/14/70 42/12/71 543 | f 42/12/71 17/14/70 18/13/72 544 | s 14 545 | f 42/9/73 18/11/74 43/6/75 546 | f 43/6/75 18/11/74 19/10/76 547 | s 15 548 | f 44/5/77 43/6/78 40/8/79 549 | f 40/8/79 43/6/78 19/7/80 550 | s off 551 | f 17/4/81 41/1/82 44/2/83 40/3/84 552 | f 68/201/85 66/186/86 41/182/87 42/187/88 553 | f 70/203/89 68/190/90 42/191/91 43/195/92 554 | f 44/194/93 72/205/94 70/203/95 43/195/96 555 | f 41/182/97 66/198/98 72/183/99 44/184/100 556 | s 16 557 | f 65/200/101 67/188/102 7/189/103 8/185/104 558 | f 67/188/102 69/193/105 2/192/106 7/189/103 559 | f 4/197/107 2/192/106 69/193/105 71/196/108 560 | f 71/196/108 65/200/101 8/185/104 4/197/107 561 | s 17 562 | f 16/46/109 15/47/110 5/60/111 563 | f 5/60/111 15/47/110 6/50/112 564 | s 18 565 | f 11/49/113 16/46/114 58/88/115 566 | f 58/88/115 16/46/114 57/89/116 567 | f 57/89/116 16/46/114 56/90/117 568 | f 56/90/117 16/46/114 55/91/118 569 | f 55/91/118 16/46/114 54/92/119 570 | f 54/92/119 16/46/114 53/93/120 571 | f 16/46/114 5/60/121 53/93/120 572 | f 11/49/113 58/88/115 1/57/122 573 | f 58/88/115 59/87/123 1/57/122 574 | f 59/87/123 60/86/124 1/57/122 575 | f 60/86/124 61/85/125 1/57/122 576 | f 61/85/125 62/84/126 1/57/122 577 | f 62/84/126 63/83/127 1/57/122 578 | f 63/83/127 64/82/128 1/57/122 579 | f 53/93/120 5/60/121 52/94/129 580 | f 64/82/128 45/101/130 1/57/122 581 | f 52/94/129 5/60/121 51/95/131 582 | f 45/101/130 46/100/132 1/57/122 583 | f 51/95/131 5/60/121 50/96/133 584 | f 46/100/132 47/99/134 1/57/122 585 | f 50/96/133 5/60/121 49/97/135 586 | f 47/99/134 48/98/136 1/57/122 587 | f 49/97/135 5/60/121 48/98/136 588 | f 48/98/136 5/60/121 1/57/122 589 | s 19 590 | f 12/48/137 11/49/138 3/54/139 591 | f 3/54/139 11/49/138 1/57/140 592 | s 20 593 | f 15/47/141 12/48/142 6/50/143 594 | f 6/50/143 12/48/142 3/54/144 595 | s 21 596 | f 25/105/145 24/104/146 58/102/147 597 | f 58/102/147 24/104/146 59/103/148 598 | f 26/109/149 25/108/145 57/106/150 599 | f 57/106/150 25/108/145 58/107/147 600 | f 27/113/151 26/112/149 56/110/152 601 | f 56/110/152 26/112/149 57/111/150 602 | f 28/117/153 27/116/151 55/114/154 603 | f 55/114/154 27/116/151 56/115/152 604 | f 29/121/155 28/120/153 54/118/156 605 | f 54/118/156 28/120/153 55/119/154 606 | f 30/125/157 29/124/155 53/122/158 607 | f 53/122/158 29/124/155 54/123/156 608 | f 31/129/159 30/128/157 52/126/160 609 | f 52/126/160 30/128/157 53/127/158 610 | f 32/133/161 31/132/159 51/130/162 611 | f 51/130/162 31/132/159 52/131/160 612 | f 33/137/163 32/136/161 50/134/164 613 | f 50/134/164 32/136/161 51/135/162 614 | f 34/141/165 33/140/163 49/138/166 615 | f 49/138/166 33/140/163 50/139/164 616 | f 35/145/167 34/144/165 48/142/168 617 | f 48/142/168 34/144/165 49/143/166 618 | f 36/149/169 35/148/167 47/146/170 619 | f 47/146/170 35/148/167 48/147/168 620 | f 37/153/171 36/152/169 46/150/172 621 | f 46/150/172 36/152/169 47/151/170 622 | f 38/157/173 37/156/171 45/154/174 623 | f 45/154/174 37/156/171 46/155/172 624 | f 39/161/175 38/160/173 64/158/176 625 | f 64/158/176 38/160/173 45/159/174 626 | f 20/165/177 39/164/175 63/162/178 627 | f 63/162/178 39/164/175 64/163/176 628 | f 21/169/179 20/168/177 62/166/180 629 | f 62/166/180 20/168/177 63/167/178 630 | f 62/173/180 61/172/181 21/170/179 631 | f 21/170/179 61/172/181 22/171/182 632 | f 61/177/181 60/176/183 22/174/182 633 | f 22/174/182 60/176/183 23/175/184 634 | f 24/181/146 23/180/184 59/178/148 635 | f 59/178/148 23/180/184 60/179/183 636 | s off 637 | f 66/198/185 65/200/186 71/199/187 72/183/188 638 | f 65/200/189 66/186/190 68/201/191 67/188/192 639 | f 67/202/193 68/190/194 70/203/195 69/204/196 640 | f 69/204/197 70/203/198 72/205/199 71/206/200 641 | -------------------------------------------------------------------------------- /model/Waveshare_2014.mb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/model/Waveshare_2014.mb -------------------------------------------------------------------------------- /o365_Test_Project/O365/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "MicroPython.executeButton": [ 3 | { 4 | "text": "▶", 5 | "tooltip": "运行", 6 | "alignment": "left", 7 | "command": "extension.executeFile", 8 | "priority": 3.5 9 | } 10 | ], 11 | "MicroPython.syncButton": [ 12 | { 13 | "text": "$(sync)", 14 | "tooltip": "同步", 15 | "alignment": "left", 16 | "command": "extension.execute", 17 | "priority": 4 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /o365_Test_Project/O365/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simple python library to interact with Microsoft Graph and Office 365 API 3 | """ 4 | #import warnings 5 | import sys 6 | 7 | from .__version__ import __version__ 8 | 9 | from .account import Account 10 | from .connection import Connection, Protocol, MSGraphProtocol, MSOffice365Protocol 11 | from .utils import FileSystemTokenBackend 12 | from .message import Message 13 | 14 | 15 | #if sys.warnoptions: 16 | # allow Deprecation warnings to appear 17 | #warnings.simplefilter('always', DeprecationWarning) 18 | -------------------------------------------------------------------------------- /o365_Test_Project/O365/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/__pycache__/__version__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/__pycache__/__version__.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/__pycache__/account.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/__pycache__/account.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/__pycache__/calendar.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/__pycache__/calendar.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/__pycache__/category.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/__pycache__/category.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/__pycache__/connection.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/__pycache__/connection.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/__pycache__/message.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/__pycache__/message.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.0.18.1' 2 | -------------------------------------------------------------------------------- /o365_Test_Project/O365/account.py: -------------------------------------------------------------------------------- 1 | from .connection import Connection, Protocol, MSGraphProtocol, MSOffice365Protocol 2 | from .utils import ME_RESOURCE, consent 3 | 4 | 5 | class Account: 6 | 7 | connection_constructor = Connection 8 | 9 | def __init__(self, credentials, *, protocol=None, main_resource=None, **kwargs): 10 | """ Creates an object which is used to access resources related to the 11 | specified credentials 12 | 13 | :param tuple credentials: a tuple containing the client_id 14 | and client_secret 15 | :param Protocol protocol: the protocol to be used in this account 16 | :param str main_resource: the resource to be used by this account 17 | ('me' or 'users', etc.) 18 | :param kwargs: any extra args to be passed to the Connection instance 19 | :raises ValueError: if an invalid protocol is passed 20 | """ 21 | 22 | protocol = protocol or MSGraphProtocol # Defaults to Graph protocol 23 | self.protocol = protocol(default_resource=main_resource, 24 | **kwargs) if isinstance(protocol, 25 | type) else protocol 26 | 27 | if not isinstance(self.protocol, Protocol): 28 | raise ValueError("'protocol' must be a subclass of Protocol") 29 | 30 | auth_flow_type = kwargs.get('auth_flow_type', 'authorization') 31 | scopes = kwargs.get('scopes', None) # retrieve scopes 32 | 33 | if auth_flow_type in ('authorization', 'public'): 34 | # convert the provided scopes to protocol scopes: 35 | if scopes is not None: 36 | kwargs['scopes'] = self.protocol.get_scopes_for(scopes) 37 | elif auth_flow_type == 'credentials': 38 | # for client credential grant flow solely: add the default scope if it's not provided 39 | if not scopes: 40 | kwargs['scopes'] = [self.protocol.prefix_scope('.default')] 41 | else: 42 | raise ValueError('Auth flow type "credentials" does not require scopes') 43 | 44 | # set main_resource to blank when it's the 'ME' resource 45 | if self.protocol.default_resource == ME_RESOURCE: 46 | self.protocol.default_resource = '' 47 | if main_resource == ME_RESOURCE: 48 | main_resource = '' 49 | else: 50 | raise ValueError('"auth_flow_type" must be "authorization", "credentials" or "public"') 51 | 52 | self.con = self.connection_constructor(credentials, **kwargs) 53 | self.main_resource = main_resource or self.protocol.default_resource 54 | 55 | def __repr__(self): 56 | if self.con.auth: 57 | return 'Account Client Id: {}'.format(self.con.auth[0]) 58 | else: 59 | return 'Unidentified Account' 60 | 61 | @property 62 | def is_authenticated(self): 63 | """ 64 | Checks whether the library has the authentication and that is not expired 65 | :return: True if authenticated, False otherwise 66 | """ 67 | token = self.con.token_backend.token 68 | if not token: 69 | token = self.con.token_backend.get_token() 70 | 71 | return token is not None and not token.is_expired 72 | 73 | def authenticate(self, *, scopes=None, handle_consent=consent.consent_input_token, **kwargs): 74 | """ Performs the oauth authentication flow using the console resulting in a stored token. 75 | It uses the credentials passed on instantiation 76 | 77 | :param list[str] or None scopes: list of protocol user scopes to be converted 78 | by the protocol or scope helpers 79 | :param kwargs: other configurations to be passed to the 80 | Connection.get_authorization_url and Connection.request_token methods 81 | :return: Success / Failure 82 | :rtype: bool 83 | """ 84 | 85 | if self.con.auth_flow_type in ('authorization', 'public'): 86 | if scopes is not None: 87 | if self.con.scopes is not None: 88 | raise RuntimeError('The scopes must be set either at the Account instantiation or on the account.authenticate method.') 89 | self.con.scopes = self.protocol.get_scopes_for(scopes) 90 | else: 91 | if self.con.scopes is None: 92 | raise ValueError('The scopes are not set. Define the scopes requested.') 93 | 94 | consent_url, _ = self.con.get_authorization_url(**kwargs) 95 | 96 | token_url = handle_consent(consent_url) 97 | 98 | if token_url: 99 | result = self.con.request_token(token_url, **kwargs) # no need to pass state as the session is the same 100 | if result: 101 | print('Authentication Flow Completed. Oauth Access Token Stored. You can now use the API.') 102 | else: 103 | print('Something go wrong. Please try again.') 104 | 105 | return bool(result) 106 | else: 107 | print('Authentication Flow aborted.') 108 | return False 109 | 110 | elif self.con.auth_flow_type == 'credentials': 111 | return self.con.request_token(None, requested_scopes=scopes) 112 | else: 113 | raise ValueError('Connection "auth_flow_type" must be "authorization", "public" or "credentials"') 114 | 115 | def get_current_user(self): 116 | """ Returns the current user """ 117 | if self.con.auth_flow_type in ('authorization', 'public'): 118 | directory = self.directory(resource=ME_RESOURCE) 119 | return directory.get_current_user() 120 | else: 121 | return None 122 | 123 | @property 124 | def connection(self): 125 | """ Alias for self.con 126 | 127 | :rtype: type(self.connection_constructor) 128 | """ 129 | return self.con 130 | 131 | def new_message(self, resource=None): 132 | """ Creates a new message to be sent or stored 133 | 134 | :param str resource: Custom resource to be used in this message 135 | (Defaults to parent main_resource) 136 | :return: New empty message 137 | :rtype: Message 138 | """ 139 | from .message import Message 140 | return Message(parent=self, main_resource=resource, is_draft=True) 141 | 142 | def mailbox(self, resource=None): 143 | """ Get an instance to the mailbox for the specified account resource 144 | 145 | :param str resource: Custom resource to be used in this mailbox 146 | (Defaults to parent main_resource) 147 | :return: a representation of account mailbox 148 | :rtype: O365.mailbox.MailBox 149 | """ 150 | from .mailbox import MailBox 151 | return MailBox(parent=self, main_resource=resource, name='MailBox') 152 | 153 | def address_book(self, *, resource=None, address_book='personal'): 154 | """ Get an instance to the specified address book for the 155 | specified account resource 156 | 157 | :param str resource: Custom resource to be used in this address book 158 | (Defaults to parent main_resource) 159 | :param str address_book: Choose from 'Personal' or 'Directory' 160 | :return: a representation of the specified address book 161 | :rtype: AddressBook or GlobalAddressList 162 | :raises RuntimeError: if invalid address_book is specified 163 | """ 164 | if address_book.lower() == 'personal': 165 | from .address_book import AddressBook 166 | 167 | return AddressBook(parent=self, main_resource=resource, 168 | name='Personal Address Book') 169 | elif address_book.lower() in ('gal', 'directory'): 170 | # for backwards compatibility only 171 | from .directory import Directory 172 | 173 | return Directory(parent=self, main_resource=resource) 174 | else: 175 | raise RuntimeError( 176 | 'address_book must be either "Personal" ' 177 | '(resource address book) or "Directory" (Active Directory)') 178 | 179 | def directory(self, resource=None): 180 | """ Returns the active directory instance""" 181 | from .directory import Directory, USERS_RESOURCE 182 | 183 | return Directory(parent=self, main_resource=resource or USERS_RESOURCE) 184 | 185 | def schedule(self, *, resource=None): 186 | """ Get an instance to work with calendar events for the 187 | specified account resource 188 | 189 | :param str resource: Custom resource to be used in this schedule object 190 | (Defaults to parent main_resource) 191 | :return: a representation of calendar events 192 | :rtype: Schedule 193 | """ 194 | from .calendar import Schedule 195 | return Schedule(parent=self, main_resource=resource) 196 | 197 | def storage(self, *, resource=None): 198 | """ Get an instance to handle file storage (OneDrive / Sharepoint) 199 | for the specified account resource 200 | 201 | :param str resource: Custom resource to be used in this drive object 202 | (Defaults to parent main_resource) 203 | :return: a representation of OneDrive File Storage 204 | :rtype: Storage 205 | :raises RuntimeError: if protocol doesn't support the feature 206 | """ 207 | if not isinstance(self.protocol, MSGraphProtocol): 208 | # TODO: Custom protocol accessing OneDrive/Sharepoint Api fails here 209 | raise RuntimeError( 210 | 'Drive options only works on Microsoft Graph API') 211 | from .drive import Storage 212 | return Storage(parent=self, main_resource=resource) 213 | 214 | def sharepoint(self, *, resource=''): 215 | """ Get an instance to read information from Sharepoint sites for the 216 | specified account resource 217 | 218 | :param str resource: Custom resource to be used in this sharepoint 219 | object (Defaults to parent main_resource) 220 | :return: a representation of Sharepoint Sites 221 | :rtype: Sharepoint 222 | :raises RuntimeError: if protocol doesn't support the feature 223 | """ 224 | 225 | if not isinstance(self.protocol, MSGraphProtocol): 226 | # TODO: Custom protocol accessing OneDrive/Sharepoint Api fails here 227 | raise RuntimeError( 228 | 'Sharepoint api only works on Microsoft Graph API') 229 | 230 | from .sharepoint import Sharepoint 231 | return Sharepoint(parent=self, main_resource=resource) 232 | 233 | def planner(self, *, resource=''): 234 | """ Get an instance to read information from Microsoft planner """ 235 | 236 | if not isinstance(self.protocol, MSGraphProtocol): 237 | # TODO: Custom protocol accessing OneDrive/Sharepoint Api fails here 238 | raise RuntimeError( 239 | 'planner api only works on Microsoft Graph API') 240 | 241 | from .planner import Planner 242 | return Planner(parent=self, main_resource=resource) 243 | 244 | def tasks(self, *, resource=''): 245 | """ Get an instance to read information from Microsoft ToDo """ 246 | 247 | if not isinstance(self.protocol, MSOffice365Protocol): 248 | raise RuntimeError( 249 | 'todo api only works on Microsoft Office 365 API') 250 | 251 | from .tasks import ToDo 252 | return ToDo(parent=self, main_resource=resource) 253 | 254 | 255 | def teams(self, *, resource=''): 256 | """ Get an instance to read information from Microsoft Teams """ 257 | 258 | if not isinstance(self.protocol, MSGraphProtocol): 259 | raise RuntimeError( 260 | 'teams api only works on Microsoft Graph API') 261 | 262 | from .teams import Teams 263 | return Teams(parent=self, main_resource=resource) 264 | 265 | def outlook_categories(self, *, resource=''): 266 | """ Returns a Categories object to handle the available Outlook Categories """ 267 | from .category import Categories 268 | 269 | return Categories(parent=self, main_resource=resource) 270 | 271 | def groups(self, *, resource=''): 272 | """ Get an instance to read information from Microsoft Groups """ 273 | 274 | if not isinstance(self.protocol, MSGraphProtocol): 275 | raise RuntimeError( 276 | 'groups api only works on Microsoft Graph API') 277 | 278 | from .groups import Groups 279 | return Groups(parent=self, main_resource=resource) 280 | -------------------------------------------------------------------------------- /o365_Test_Project/O365/category.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from .utils import ApiComponent 4 | 5 | 6 | class CategoryColor(Enum): 7 | RED = 'preset0' # 0 8 | ORANGE = 'preset1' # 1 9 | BROWN = 'preset2' # 2 10 | YELLOW = 'preset3' # 3 11 | GREEN = 'preset4' # 4 12 | TEAL = 'preset5' # 5 13 | OLIVE = 'preset6' # 6 14 | BLUE = 'preset7' # 7 15 | PURPLE = 'preset8' # 8 16 | CRANBERRY = 'preset9' # 9 17 | STEEL = 'preset10' # 10 18 | DARKSTEEL = 'preset11' # 11 19 | GRAY = 'preset12' # 12 20 | DARKGREY = 'preset13' # 13 21 | BLACK = 'preset14' # 14 22 | DARKRED = 'preset15' # 15 23 | DARKORANGE = 'preset16' # 16 24 | DARKBROWN = 'preset17' # 17 25 | DARKYELLOW = 'preset18' # 18 26 | DARKGREEN = 'preset19' # 19 27 | DARKTEAL = 'preset20' # 20 28 | DARKOLIVE = 'preset21' # 21 29 | DARKBLUE = 'preset22' # 22 30 | DARKPURPLE = 'preset23' # 23 31 | DARKCRANBERRY = 'preset24' # 24 32 | 33 | @classmethod 34 | def get(cls, color): 35 | """ 36 | Gets a color by name or value. 37 | Raises ValueError if not found whithin the collection of colors. 38 | """ 39 | try: 40 | return cls(color.capitalize()) # 'preset0' to 'Preset0' 41 | except ValueError: 42 | pass 43 | try: 44 | return cls[color.upper()] # 'red' to 'RED' 45 | except KeyError: 46 | raise ValueError('color is not a valid color from CategoryColor') from None 47 | 48 | 49 | class Category(ApiComponent): 50 | 51 | _endpoints = { 52 | 'update': '/outlook/masterCategories/{id}' 53 | } 54 | 55 | def __init__(self, *, parent=None, con=None, **kwargs): 56 | """ 57 | Represents a category by which a user can group Outlook 58 | items such as messages and events. 59 | It can be used in conjunction with Event, Message, Contact and Post. 60 | 61 | :param parent: parent object 62 | :type parent: Account 63 | :param Connection con: connection to use if no parent specified 64 | :param Protocol protocol: protocol to use if no parent specified 65 | (kwargs) 66 | :param str main_resource: use this resource instead of parent resource 67 | (kwargs) 68 | """ 69 | 70 | if parent and con: 71 | raise ValueError('Need a parent or a connection but not both') 72 | self.con = parent.con if parent else con 73 | 74 | # Choose the main_resource passed in kwargs over parent main_resource 75 | main_resource = kwargs.pop('main_resource', None) or ( 76 | getattr(parent, 'main_resource', None) if parent else None) 77 | 78 | super().__init__( 79 | protocol=parent.protocol if parent else kwargs.get('protocol'), 80 | main_resource=main_resource) 81 | 82 | cloud_data = kwargs.get(self._cloud_data_key, {}) 83 | 84 | self.object_id = cloud_data.get('id') 85 | self.name = cloud_data.get(self._cc('displayName')) 86 | color = cloud_data.get(self._cc('color')) 87 | self.color = CategoryColor(color) if color else None 88 | 89 | def __str__(self): 90 | return self.__repr__() 91 | 92 | def __repr__(self): 93 | return '{} (color: {})'.format(self.name, self.color.name if self.color else None) 94 | 95 | def update_color(self, color): 96 | """ 97 | Updates this Category color 98 | :param None or str or CategoryColor color: the category color 99 | """ 100 | url = self.build_url(self._endpoints.get('update').format(id=self.object_id)) 101 | if color is not None and not isinstance(color, CategoryColor): 102 | color = CategoryColor.get(color) 103 | 104 | response = self.con.patch(url, data={'color': color.value if color else None}) 105 | if not response: 106 | return False 107 | 108 | self.color = color 109 | return True 110 | 111 | def delete(self): 112 | """ Deletes this Category """ 113 | url = self.build_url(self._endpoints.get('update').format(id=self.object_id)) 114 | 115 | response = self.con.delete(url) 116 | 117 | return bool(response) 118 | 119 | 120 | class Categories(ApiComponent): 121 | 122 | _endpoints = { 123 | 'list': '/outlook/masterCategories', 124 | 'get': '/outlook/masterCategories/{id}', 125 | } 126 | 127 | category_constructor = Category 128 | 129 | def __init__(self, *, parent=None, con=None, **kwargs): 130 | """ Object to retrive categories 131 | 132 | :param parent: parent object 133 | :type parent: Account 134 | :param Connection con: connection to use if no parent specified 135 | :param Protocol protocol: protocol to use if no parent specified 136 | (kwargs) 137 | :param str main_resource: use this resource instead of parent resource 138 | (kwargs) 139 | """ 140 | 141 | if parent and con: 142 | raise ValueError('Need a parent or a connection but not both') 143 | self.con = parent.con if parent else con 144 | 145 | # Choose the main_resource passed in kwargs over parent main_resource 146 | main_resource = kwargs.pop('main_resource', None) or ( 147 | getattr(parent, 'main_resource', None) if parent else None) 148 | 149 | super().__init__( 150 | protocol=parent.protocol if parent else kwargs.get('protocol'), 151 | main_resource=main_resource) 152 | 153 | def get_categories(self): 154 | """ Returns a list of categories""" 155 | url = self.build_url(self._endpoints.get('list')) 156 | 157 | response = self.con.get(url) 158 | if not response: 159 | return [] 160 | 161 | data = response.json() 162 | 163 | return [ 164 | self.category_constructor(parent=self, **{self._cloud_data_key: category}) 165 | for category in data.get('value', []) 166 | ] 167 | 168 | def get_category(self, category_id): 169 | """ Returns a category by id""" 170 | url = self.build_url(self._endpoints.get('get').format(id=category_id)) 171 | 172 | response = self.con.get(url) 173 | if not response: 174 | return None 175 | 176 | data = response.json() 177 | 178 | return self.category_constructor(parent=self, **{self._cloud_data_key: data}) 179 | 180 | def create_category(self, name, color='auto'): 181 | """ 182 | Creates a category. 183 | If the color is not provided it will be choosed from the pool of unused colors. 184 | 185 | :param str name: The name of this outlook category. Must be unique. 186 | :param str or CategoryColor color: optional color. If not provided will be assigned automatically. 187 | :return: bool 188 | """ 189 | if color == 'auto': 190 | used_colors = {category.color for category in self.get_categories()} 191 | all_colors = {color for color in CategoryColor} 192 | available_colors = all_colors - used_colors 193 | try: 194 | color = available_colors.pop() 195 | except KeyError: 196 | # re-use a color 197 | color = all_colors.pop() 198 | else: 199 | if color is not None and not isinstance(color, CategoryColor): 200 | color = CategoryColor.get(color) 201 | 202 | url = self.build_url(self._endpoints.get('list')) 203 | data = {self._cc('displayName'): name, 'color': color.value if color else None} 204 | response = self.con.post(url, data=data) 205 | if not response: 206 | return None 207 | 208 | category = response.json() 209 | 210 | return self.category_constructor(parent=self, **{self._cloud_data_key: category}) 211 | -------------------------------------------------------------------------------- /o365_Test_Project/O365/directory.py: -------------------------------------------------------------------------------- 1 | ##import logging 2 | from dateutil.parser import parse 3 | from requests.exceptions import HTTPError 4 | from .message import Message, RecipientType 5 | from .utils import ApiComponent, NEXT_LINK_KEYWORD, Pagination, ME_RESOURCE 6 | 7 | USERS_RESOURCE = 'users' 8 | 9 | #log = logging.getLogger(__name__) 10 | 11 | 12 | class User(ApiComponent): 13 | 14 | _endpoints = { 15 | 'photo': '/photo/$value', 16 | 'photo_size': '/photos/{size}/$value' 17 | } 18 | 19 | message_constructor = Message 20 | 21 | def __init__(self, *, parent=None, con=None, **kwargs): 22 | """ Represents an Azure AD user account 23 | 24 | :param parent: parent object 25 | :type parent: Account 26 | :param Connection con: connection to use if no parent specified 27 | :param Protocol protocol: protocol to use if no parent specified 28 | (kwargs) 29 | :param str main_resource: use this resource instead of parent resource 30 | (kwargs) 31 | """ 32 | 33 | if parent and con: 34 | raise ValueError('Need a parent or a connection but not both') 35 | self.con = parent.con if parent else con 36 | 37 | # Choose the main_resource passed in kwargs over parent main_resource 38 | main_resource = kwargs.pop('main_resource', None) or ( 39 | getattr(parent, 'main_resource', None) if parent else None) 40 | 41 | cloud_data = kwargs.get(self._cloud_data_key, {}) 42 | 43 | self.object_id = cloud_data.get('id') 44 | 45 | if main_resource == USERS_RESOURCE: 46 | main_resource += '/{}'.format(self.object_id) 47 | 48 | super().__init__( 49 | protocol=parent.protocol if parent else kwargs.get('protocol'), 50 | main_resource=main_resource) 51 | 52 | local_tz = self.protocol.timezone 53 | cc = self._cc 54 | 55 | self.type = cloud_data.get('@odata.type') 56 | self.user_principal_name = cloud_data.get(cc('userPrincipalName')) 57 | self.display_name = cloud_data.get(cc('displayName')) 58 | self.given_name = cloud_data.get(cc('givenName'), '') 59 | self.surname = cloud_data.get(cc('surname'), '') 60 | self.mail = cloud_data.get(cc('mail')) # read only 61 | self.business_phones = cloud_data.get(cc('businessPhones'), []) 62 | self.job_title = cloud_data.get(cc('jobTitle')) 63 | self.mobile_phone = cloud_data.get(cc('mobilePhone')) 64 | self.office_location = cloud_data.get(cc('officeLocation')) 65 | self.preferred_language = cloud_data.get(cc('preferredLanguage')) 66 | # End of default properties. Next properties must be selected 67 | 68 | self.about_me = cloud_data.get(cc('aboutMe')) 69 | self.account_enabled = cloud_data.get(cc('accountEnabled')) 70 | self.age_group = cloud_data.get(cc('ageGroup')) 71 | self.assigned_licenses = cloud_data.get(cc('assignedLicenses')) 72 | self.assigned_plans = cloud_data.get(cc('assignedPlans')) # read only 73 | birthday = cloud_data.get(cc('birthday')) 74 | self.birthday = parse(birthday).astimezone(local_tz) if birthday else None 75 | self.city = cloud_data.get(cc('city')) 76 | self.company_name = cloud_data.get(cc('companyName')) 77 | self.consent_provided_for_minor = cloud_data.get(cc('consentProvidedForMinor')) 78 | self.country = cloud_data.get(cc('country')) 79 | created = cloud_data.get(cc('createdDateTime')) 80 | self.created = parse(created).astimezone( 81 | local_tz) if created else None 82 | self.department = cloud_data.get(cc('department')) 83 | self.employee_id = cloud_data.get(cc('employeeId')) 84 | self.fax_number = cloud_data.get(cc('faxNumber')) 85 | hire_date = cloud_data.get(cc('hireDate')) 86 | self.hire_date = parse(hire_date).astimezone( 87 | local_tz) if hire_date else None 88 | self.im_addresses = cloud_data.get(cc('imAddresses')) # read only 89 | self.interests = cloud_data.get(cc('interests')) 90 | self.is_resource_account = cloud_data.get(cc('isResourceAccount')) 91 | last_password_change = cloud_data.get(cc('lastPasswordChangeDateTime')) 92 | self.last_password_change = parse(last_password_change).astimezone( 93 | local_tz) if last_password_change else None 94 | self.legal_age_group_classification = cloud_data.get(cc('legalAgeGroupClassification')) 95 | self.license_assignment_states = cloud_data.get(cc('licenseAssignmentStates')) # read only 96 | self.mailbox_settings = cloud_data.get(cc('mailboxSettings')) 97 | self.mail_nickname = cloud_data.get(cc('mailNickname')) 98 | self.my_site = cloud_data.get(cc('mySite')) 99 | self.other_mails = cloud_data.get(cc('otherMails')) 100 | self.password_policies = cloud_data.get(cc('passwordPolicies')) 101 | self.password_profile = cloud_data.get(cc('passwordProfile')) 102 | self.past_projects = cloud_data.get(cc('pastProjects')) 103 | self.postal_code = cloud_data.get(cc('postalCode')) 104 | self.preferred_data_location = cloud_data.get(cc('preferredDataLocation')) 105 | self.preferred_name = cloud_data.get(cc('preferredName')) 106 | self.provisioned_plans = cloud_data.get(cc('provisionedPlans')) # read only 107 | self.proxy_addresses = cloud_data.get(cc('proxyAddresses')) # read only 108 | self.responsibilities = cloud_data.get(cc('responsibilities')) 109 | self.schools = cloud_data.get(cc('schools')) 110 | self.show_in_address_list = cloud_data.get(cc('showInAddressList'), True) 111 | self.skills = cloud_data.get(cc('skills')) 112 | sign_in_sessions_valid_from = cloud_data.get(cc('signInSessionsValidFromDateTime')) # read only 113 | self.sign_in_sessions_valid_from = parse(sign_in_sessions_valid_from).astimezone( 114 | local_tz) if sign_in_sessions_valid_from else None 115 | self.state = cloud_data.get(cc('state')) 116 | self.street_address = cloud_data.get(cc('streetAddress')) 117 | self.usage_location = cloud_data.get(cc('usageLocation')) 118 | self.user_type = cloud_data.get(cc('userType')) 119 | 120 | def __str__(self): 121 | return self.__repr__() 122 | 123 | def __repr__(self): 124 | return self.display_name or self.full_name or self.user_principal_name or 'Unknown Name' 125 | 126 | def __eq__(self, other): 127 | return self.object_id == other.object_id 128 | 129 | def __hash__(self): 130 | return self.object_id.__hash__() 131 | 132 | @property 133 | def full_name(self): 134 | """ Full Name (Name + Surname) 135 | :rtype: str 136 | """ 137 | return '{} {}'.format(self.given_name, self.surname).strip() 138 | 139 | def new_message(self, recipient=None, *, recipient_type=RecipientType.TO): 140 | """ This method returns a new draft Message instance with this 141 | user email as a recipient 142 | 143 | :param Recipient recipient: a Recipient instance where to send this 144 | message. If None the email of this contact will be used 145 | :param RecipientType recipient_type: section to add recipient into 146 | :return: newly created message 147 | :rtype: Message or None 148 | """ 149 | 150 | if isinstance(recipient_type, str): 151 | recipient_type = RecipientType(recipient_type) 152 | 153 | recipient = recipient or self.mail 154 | if not recipient: 155 | return None 156 | 157 | new_message = self.message_constructor(parent=self, is_draft=True) 158 | 159 | target_recipients = getattr(new_message, str(recipient_type.value)) 160 | target_recipients.add(recipient) 161 | 162 | return new_message 163 | 164 | def get_profile_photo(self, size=None): 165 | """ Returns the user profile photo 166 | :param str size: 48x48, 64x64, 96x96, 120x120, 240x240, 167 | 360x360, 432x432, 504x504, and 648x648 168 | """ 169 | if size is None: 170 | url = self.build_url(self._endpoints.get('photo')) 171 | else: 172 | url = self.build_url(self._endpoints.get('photo_size').format(size=size)) 173 | 174 | try: 175 | response = self.con.get(url) 176 | except HTTPError as e: 177 | #log.debug('Error while retrieving the user profile photo. Error: {}'.format(e)) 178 | return None 179 | 180 | if not response: 181 | return None 182 | 183 | return response.content 184 | 185 | def update_profile_photo(self, photo): 186 | """ Updates this user profile photo 187 | :param bytes photo: the photo data in bytes 188 | """ 189 | 190 | url = self.build_url(self._endpoints.get('photo')) 191 | response = self.con.patch(url, data=photo, headers={'Content-type': 'image/jpeg'}) 192 | 193 | return bool(response) 194 | 195 | 196 | class Directory(ApiComponent): 197 | 198 | _endpoints = { 199 | 'get_user': '/{email}' 200 | } 201 | user_constructor = User 202 | 203 | def __init__(self, *, parent=None, con=None, **kwargs): 204 | """ Represents the Active Directory 205 | 206 | :param parent: parent object 207 | :type parent: Account 208 | :param Connection con: connection to use if no parent specified 209 | :param Protocol protocol: protocol to use if no parent specified 210 | (kwargs) 211 | :param str main_resource: use this resource instead of parent resource 212 | (kwargs) 213 | """ 214 | 215 | if parent and con: 216 | raise ValueError('Need a parent or a connection but not both') 217 | self.con = parent.con if parent else con 218 | 219 | # Choose the main_resource passed in kwargs over parent main_resource 220 | main_resource = kwargs.pop('main_resource', None) or ( 221 | getattr(parent, 'main_resource', None) if parent else None) 222 | 223 | super().__init__( 224 | protocol=parent.protocol if parent else kwargs.get('protocol'), 225 | main_resource=main_resource) 226 | 227 | def __repr__(self): 228 | return 'Active Directory' 229 | 230 | def get_users(self, limit=100, *, query=None, order_by=None, batch=None): 231 | """ Gets a list of users from the active directory 232 | 233 | When querying the Active Directory the Users endpoint will be used. 234 | Only a limited set of information will be available unless you have 235 | access to scope 'User.Read.All' which requires App Administration 236 | Consent. 237 | 238 | Also using endpoints has some limitations on the querying capabilities. 239 | 240 | To use query an order_by check the OData specification here: 241 | http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/ 242 | part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions 243 | -complete.html 244 | 245 | :param limit: max no. of contacts to get. Over 999 uses batch. 246 | :type limit: int or None 247 | :param query: applies a OData filter to the request 248 | :type query: Query or str 249 | :param order_by: orders the result set based on this condition 250 | :type order_by: Query or str 251 | :param int batch: batch size, retrieves items in 252 | batches allowing to retrieve more items than the limit. 253 | :return: list of users 254 | :rtype: list[User] or Pagination 255 | """ 256 | 257 | url = self.build_url('') # target the main_resource 258 | 259 | if limit is None or limit > self.protocol.max_top_value: 260 | batch = self.protocol.max_top_value 261 | 262 | params = {'$top': batch if batch else limit} 263 | 264 | if order_by: 265 | params['$orderby'] = order_by 266 | 267 | if query: 268 | if isinstance(query, str): 269 | params['$filter'] = query 270 | else: 271 | params.update(query.as_params()) 272 | 273 | response = self.con.get(url, params=params) 274 | if not response: 275 | return iter(()) 276 | 277 | data = response.json() 278 | 279 | # Everything received from cloud must be passed as self._cloud_data_key 280 | users = (self.user_constructor(parent=self, **{self._cloud_data_key: user}) 281 | for user in data.get('value', [])) 282 | 283 | next_link = data.get(NEXT_LINK_KEYWORD, None) 284 | 285 | if batch and next_link: 286 | return Pagination(parent=self, data=users, 287 | constructor=self.user_constructor, 288 | next_link=next_link, limit=limit) 289 | else: 290 | return users 291 | 292 | def _get_user(self, url, query=None): 293 | """Helper method so DRY""" 294 | 295 | params = {} 296 | if query: 297 | if isinstance(query, str): 298 | params['$filter'] = query 299 | else: 300 | params.update(query.as_params()) 301 | 302 | response = self.con.get(url, params=params) 303 | if not response: 304 | return None 305 | 306 | data = response.json() 307 | 308 | # Everything received from cloud must be passed as self._cloud_data_key 309 | return self.user_constructor(parent=self, **{self._cloud_data_key: data}) 310 | 311 | def get_user(self, user, query=None): 312 | """ Returns a User by it's id or user principal name 313 | 314 | :param str user: the user id or user principal name 315 | :return: User for specified email 316 | :rtype: User 317 | """ 318 | url = self.build_url(self._endpoints.get('get_user').format(email=user)) 319 | return self._get_user(url, query=query) 320 | 321 | def get_current_user(self, query=None): 322 | """ Returns the current logged in user""" 323 | 324 | if self.main_resource != ME_RESOURCE: 325 | raise ValueError("Can't get the current user. The main resource must be set to '{}'".format(ME_RESOURCE)) 326 | 327 | url = self.build_url('') # target main_resource 328 | return self._get_user(url, query=query) 329 | 330 | def get_user_manager(self, user, query=None): 331 | """ Returns a Users' manager by the users id, or user principal name 332 | 333 | :param str user: the user id or user principal name 334 | :return: User for specified email 335 | :rtype: User 336 | """ 337 | url = self.build_url(self._endpoints.get('get_user').format(email=user)) 338 | return self._get_user(url + '/manager', query=query) 339 | 340 | def get_user_direct_reports(self, user, limit=100, *, query=None, order_by=None, batch=None): 341 | """ Gets a list of direct reports for the user provided from the active directory 342 | 343 | When querying the Active Directory the Users endpoint will be used. 344 | 345 | Also using endpoints has some limitations on the querying capabilities. 346 | 347 | To use query an order_by check the OData specification here: 348 | http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/ 349 | part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions 350 | -complete.html 351 | 352 | :param limit: max no. of contacts to get. Over 999 uses batch. 353 | :type limit: int or None 354 | :param query: applies a OData filter to the request 355 | :type query: Query or str 356 | :param order_by: orders the result set based on this condition 357 | :type order_by: Query or str 358 | :param int batch: batch size, retrieves items in 359 | batches allowing to retrieve more items than the limit. 360 | :return: list of users 361 | :rtype: list[User] or Pagination 362 | """ 363 | 364 | url = self.build_url(self._endpoints.get('get_user').format(email=user)) 365 | 366 | if limit is None or limit > self.protocol.max_top_value: 367 | batch = self.protocol.max_top_value 368 | 369 | params = {'$top': batch if batch else limit} 370 | 371 | if order_by: 372 | params['$orderby'] = order_by 373 | 374 | if query: 375 | if isinstance(query, str): 376 | params['$filter'] = query 377 | else: 378 | params.update(query.as_params()) 379 | 380 | response = self.con.get(url + '/directReports', params=params) 381 | if not response: 382 | return iter(()) 383 | 384 | data = response.json() 385 | 386 | # Everything received from cloud must be passed as self._cloud_data_key 387 | direct_reports = (self.user_constructor(parent=self, **{self._cloud_data_key: user}) 388 | for user in data.get('value', [])) 389 | 390 | next_link = data.get(NEXT_LINK_KEYWORD, None) 391 | 392 | if batch and next_link: 393 | return Pagination(parent=self, data=direct_reports, 394 | constructor=self.user_constructor, 395 | next_link=next_link, limit=limit) 396 | else: 397 | return direct_reports 398 | -------------------------------------------------------------------------------- /o365_Test_Project/O365/groups.py: -------------------------------------------------------------------------------- 1 | #import logging 2 | 3 | from dateutil.parser import parse 4 | from .utils import ApiComponent 5 | from .directory import User 6 | 7 | #log = logging.getLogger(__name__) 8 | 9 | class Group(ApiComponent): 10 | """ A Microsoft O365 group """ 11 | 12 | _endpoints = { 13 | 'get_group_owners': '/groups/{group_id}/owners', 14 | 'get_group_members': '/groups/{group_id}/members', 15 | } 16 | 17 | member_constructor = User 18 | 19 | def __init__(self, *, parent=None, con=None, **kwargs): 20 | """ A Microsoft O365 group 21 | 22 | :param parent: parent object 23 | :type parent: Teams 24 | :param Connection con: connection to use if no parent specified 25 | :param Protocol protocol: protocol to use if no parent specified 26 | (kwargs) 27 | :param str main_resource: use this resource instead of parent resource 28 | (kwargs) 29 | """ 30 | if parent and con: 31 | raise ValueError('Need a parent or a connection but not both') 32 | self.con = parent.con if parent else con 33 | 34 | cloud_data = kwargs.get(self._cloud_data_key, {}) 35 | 36 | self.object_id = cloud_data.get('id') 37 | 38 | # Choose the main_resource passed in kwargs over parent main_resource 39 | main_resource = kwargs.pop('main_resource', None) or ( 40 | getattr(parent, 'main_resource', None) if parent else None) 41 | 42 | main_resource = '{}{}'.format(main_resource, '') 43 | 44 | super().__init__( 45 | protocol=parent.protocol if parent else kwargs.get('protocol'), 46 | main_resource=main_resource) 47 | 48 | self.type = cloud_data.get('@odata.type') 49 | self.display_name = cloud_data.get(self._cc('displayName'), '') 50 | self.description = cloud_data.get(self._cc('description'), '') 51 | self.mail = cloud_data.get(self._cc('mail'), '') 52 | self.mail_nickname = cloud_data.get(self._cc('mailNickname'), '') 53 | self.visibility = cloud_data.get(self._cc('visibility'), '') 54 | 55 | def __str__(self): 56 | return self.__repr__() 57 | 58 | def __repr__(self): 59 | return 'Group: {}'.format(self.display_name) 60 | 61 | def __eq__(self, other): 62 | return self.object_id == other.object_id 63 | 64 | def __hash__(self): 65 | return self.object_id.__hash__() 66 | 67 | def get_group_members(self, recursive=False): 68 | """ Returns members of given group 69 | :param bool recursive: drill down to users if group has other group as a member 70 | :rtype: list[User] 71 | """ 72 | if recursive: 73 | recursive_data = self._get_group_members_raw() 74 | for member in recursive_data: 75 | if member['@odata.type'] == '#microsoft.graph.group': 76 | recursive_members = Groups(con=self.con, protocol=self.protocol).get_group_by_id(member['id'])._get_group_members_raw() 77 | recursive_data.extend(recursive_members) 78 | return [self.member_constructor(parent=self, **{self._cloud_data_key: lst}) for lst in recursive_data] 79 | else: 80 | return [self.member_constructor(parent=self, **{self._cloud_data_key: lst}) for lst in self._get_group_members_raw()] 81 | 82 | def _get_group_members_raw(self): 83 | url = self.build_url(self._endpoints.get('get_group_members').format(group_id=self.object_id)) 84 | 85 | response = self.con.get(url) 86 | if not response: 87 | return [] 88 | 89 | data = response.json() 90 | return data.get('value', []) 91 | 92 | def get_group_owners(self): 93 | """ Returns owners of given group 94 | 95 | :rtype: list[User] 96 | """ 97 | url = self.build_url(self._endpoints.get('get_group_owners').format(group_id=self.object_id)) 98 | 99 | response = self.con.get(url) 100 | if not response: 101 | return [] 102 | 103 | data = response.json() 104 | 105 | return [self.member_constructor(parent=self, **{self._cloud_data_key: lst}) for lst in data.get('value', [])] 106 | 107 | class Groups(ApiComponent): 108 | """ A microsoft groups class 109 | In order to use the API following permissions are required. 110 | Delegated (work or school account) - Group.Read.All, Group.ReadWrite.All 111 | """ 112 | 113 | _endpoints = { 114 | 'get_user_groups': '/users/{user_id}/memberOf', 115 | 'get_group_by_id': '/groups/{group_id}', 116 | 'get_group_by_mail': '/groups/?$search="mail:{group_mail}"&$count=true' 117 | } 118 | 119 | group_constructor = Group 120 | 121 | def __init__(self, *, parent=None, con=None, **kwargs): 122 | """ A Teams object 123 | 124 | :param parent: parent object 125 | :type parent: Account 126 | :param Connection con: connection to use if no parent specified 127 | :param Protocol protocol: protocol to use if no parent specified 128 | (kwargs) 129 | :param str main_resource: use this resource instead of parent resource 130 | (kwargs) 131 | """ 132 | if parent and con: 133 | raise ValueError('Need a parent or a connection but not both') 134 | self.con = parent.con if parent else con 135 | 136 | # Choose the main_resource passed in kwargs over the host_name 137 | main_resource = kwargs.pop('main_resource', 138 | '') # defaults to blank resource 139 | super().__init__( 140 | protocol=parent.protocol if parent else kwargs.get('protocol'), 141 | main_resource=main_resource) 142 | 143 | def __str__(self): 144 | return self.__repr__() 145 | 146 | def __repr__(self): 147 | return 'Microsoft O365 Group parent class' 148 | 149 | def get_group_by_id(self, group_id = None): 150 | """ Returns Microsoft O365/AD group with given id 151 | 152 | :param group_id: group id of group 153 | 154 | :rtype: Group 155 | """ 156 | 157 | if not group_id: 158 | raise RuntimeError('Provide the group_id') 159 | 160 | if group_id: 161 | # get channels by the team id 162 | url = self.build_url( 163 | self._endpoints.get('get_group_by_id').format(group_id=group_id)) 164 | 165 | response = self.con.get(url) 166 | 167 | if not response: 168 | return None 169 | 170 | data = response.json() 171 | 172 | return self.group_constructor(parent=self, 173 | **{self._cloud_data_key: data}) 174 | 175 | def get_group_by_mail(self, group_mail = None): 176 | """ Returns Microsoft O365/AD group by mail field 177 | 178 | :param group_name: mail of group 179 | 180 | :rtype: Group 181 | """ 182 | if not group_mail: 183 | raise RuntimeError('Provide the group mail') 184 | 185 | if group_mail: 186 | # get groups by filter mail 187 | url = self.build_url( 188 | self._endpoints.get('get_group_by_mail').format(group_mail=group_mail)) 189 | 190 | response = self.con.get(url, headers={'ConsistencyLevel': 'eventual'}) 191 | 192 | if not response: 193 | return None 194 | 195 | data = response.json() 196 | 197 | if '@odata.count' in data and data['@odata.count'] < 1: 198 | raise RuntimeError('Not found group with provided filters') 199 | 200 | # mail is unique field so, we expect exact match -> always use first element from list 201 | return self.group_constructor(parent=self, 202 | **{self._cloud_data_key: data.get('value')[0]}) 203 | 204 | def get_user_groups(self, user_id = None): 205 | """ Returns list of groups that given user has membership 206 | 207 | :param user_id: user_id 208 | 209 | :rtype: list[Group] 210 | """ 211 | 212 | if not user_id: 213 | raise RuntimeError('Provide the user_id') 214 | 215 | if user_id: 216 | # get channels by the team id 217 | url = self.build_url( 218 | self._endpoints.get('get_user_groups').format(user_id=user_id)) 219 | 220 | response = self.con.get(url) 221 | 222 | if not response: 223 | return None 224 | 225 | data = response.json() 226 | 227 | return [ 228 | self.group_constructor(parent=self, **{self._cloud_data_key: group}) 229 | for group in data.get('value', [])] 230 | -------------------------------------------------------------------------------- /o365_Test_Project/O365/mailbox.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | #import logging 3 | 4 | from .message import Message 5 | from .utils import Pagination, NEXT_LINK_KEYWORD, \ 6 | OutlookWellKnowFolderNames, ApiComponent 7 | 8 | #log = logging.getLogger(__name__) 9 | 10 | 11 | class Folder(ApiComponent): 12 | """ A Mail Folder representation """ 13 | 14 | _endpoints = { 15 | 'root_folders': '/mailFolders', 16 | 'child_folders': '/mailFolders/{id}/childFolders', 17 | 'get_folder': '/mailFolders/{id}', 18 | 'root_messages': '/messages', 19 | 'folder_messages': '/mailFolders/{id}/messages', 20 | 'copy_folder': '/mailFolders/{id}/copy', 21 | 'move_folder': '/mailFolders/{id}/move', 22 | 'message': '/messages/{id}', 23 | } 24 | message_constructor = Message 25 | 26 | def __init__(self, *, parent=None, con=None, **kwargs): 27 | """ Create an instance to represent the specified folder un given 28 | parent folder 29 | 30 | :param parent: parent folder/account for this folder 31 | :type parent: mailbox.Folder or Account 32 | :param Connection con: connection to use if no parent specified 33 | :param Protocol protocol: protocol to use if no parent specified 34 | (kwargs) 35 | :param str main_resource: use this resource instead of parent resource 36 | (kwargs) 37 | :param str name: name of the folder to get under the parent (kwargs) 38 | :param str folder_id: id of the folder to get under the parent (kwargs) 39 | """ 40 | if parent and con: 41 | raise ValueError('Need a parent or a connection but not both') 42 | self.con = parent.con if parent else con 43 | self.parent = parent if isinstance(parent, Folder) else None 44 | 45 | # This folder has no parents if root = True. 46 | self.root = kwargs.pop('root', False) 47 | 48 | # Choose the main_resource passed in kwargs over parent main_resource 49 | main_resource = kwargs.pop('main_resource', None) or ( 50 | getattr(parent, 'main_resource', None) if parent else None) 51 | 52 | super().__init__( 53 | protocol=parent.protocol if parent else kwargs.get('protocol'), 54 | main_resource=main_resource) 55 | 56 | cloud_data = kwargs.get(self._cloud_data_key, {}) 57 | 58 | # Fallback to manual folder if nothing available on cloud data 59 | self.name = cloud_data.get(self._cc('displayName'), 60 | kwargs.get('name', 61 | '')) 62 | if self.root is False: 63 | # Fallback to manual folder if nothing available on cloud data 64 | self.folder_id = cloud_data.get(self._cc('id'), 65 | kwargs.get('folder_id', 66 | None)) 67 | self.parent_id = cloud_data.get(self._cc('parentFolderId'), None) 68 | self.child_folders_count = cloud_data.get( 69 | self._cc('childFolderCount'), 0) 70 | self.unread_items_count = cloud_data.get( 71 | self._cc('unreadItemCount'), 0) 72 | self.total_items_count = cloud_data.get(self._cc('totalItemCount'), 73 | 0) 74 | self.updated_at = dt.datetime.now() 75 | else: 76 | self.folder_id = 'root' 77 | 78 | def __str__(self): 79 | return self.__repr__() 80 | 81 | def __repr__(self): 82 | return '{} from resource: {}'.format(self.name, self.main_resource) 83 | 84 | def __eq__(self, other): 85 | return self.folder_id == other.folder_id 86 | 87 | def get_folders(self, limit=None, *, query=None, order_by=None, batch=None): 88 | """ Returns a list of child folders matching the query 89 | 90 | :param int limit: max no. of folders to get. Over 999 uses batch. 91 | :param query: applies a filter to the request such as 92 | "displayName eq 'HelloFolder'" 93 | :type query: Query or str 94 | :param order_by: orders the result set based on this condition 95 | :type order_by: Query or str 96 | :param int batch: batch size, retrieves items in 97 | batches allowing to retrieve more items than the limit. 98 | :return: list of folders 99 | :rtype: list[mailbox.Folder] or Pagination 100 | """ 101 | 102 | if self.root: 103 | url = self.build_url(self._endpoints.get('root_folders')) 104 | else: 105 | url = self.build_url( 106 | self._endpoints.get('child_folders').format(id=self.folder_id)) 107 | 108 | if limit is None or limit > self.protocol.max_top_value: 109 | batch = self.protocol.max_top_value 110 | 111 | params = {'$top': batch if batch else limit} 112 | 113 | if order_by: 114 | params['$orderby'] = order_by 115 | 116 | if query: 117 | if isinstance(query, str): 118 | params['$filter'] = query 119 | else: 120 | params.update(query.as_params()) 121 | 122 | response = self.con.get(url, params=params) 123 | if not response: 124 | return [] 125 | 126 | data = response.json() 127 | 128 | # Everything received from cloud must be passed as self._cloud_data_key 129 | self_class = getattr(self, 'folder_constructor', type(self)) 130 | folders = [self_class(parent=self, **{self._cloud_data_key: folder}) for 131 | folder in data.get('value', [])] 132 | next_link = data.get(NEXT_LINK_KEYWORD, None) 133 | if batch and next_link: 134 | return Pagination(parent=self, data=folders, constructor=self_class, 135 | next_link=next_link, limit=limit) 136 | else: 137 | return folders 138 | 139 | def get_message(self, object_id=None, query=None, *, download_attachments=False): 140 | """ Get one message from the query result. 141 | A shortcut to get_messages with limit=1 142 | :param object_id: the message id to be retrieved. 143 | :param query: applies a filter to the request such as 144 | "displayName eq 'HelloFolder'" 145 | :type query: Query or str 146 | :param bool download_attachments: whether or not to download attachments 147 | :return: one Message 148 | :rtype: Message or None 149 | """ 150 | if object_id is None and query is None: 151 | raise ValueError('Must provide object id or query.') 152 | 153 | if object_id is not None: 154 | url = self.build_url(self._endpoints.get('message').format(id=object_id)) 155 | params = None 156 | if query and (query.has_selects or query.has_expands): 157 | params = query.as_params() 158 | response = self.con.get(url, params=params) 159 | if not response: 160 | return None 161 | 162 | message = response.json() 163 | 164 | return self.message_constructor(parent=self, 165 | download_attachments=download_attachments, 166 | **{self._cloud_data_key: message}) 167 | 168 | else: 169 | messages = list(self.get_messages(limit=1, query=query, 170 | download_attachments=download_attachments)) 171 | 172 | return messages[0] if messages else None 173 | 174 | def get_messages(self, limit=25, *, query=None, order_by=None, batch=None, 175 | download_attachments=False): 176 | """ 177 | Downloads messages from this folder 178 | 179 | :param int limit: limits the result set. Over 999 uses batch. 180 | :param query: applies a filter to the request such as 181 | "displayName eq 'HelloFolder'" 182 | :type query: Query or str 183 | :param order_by: orders the result set based on this condition 184 | :type order_by: Query or str 185 | :param int batch: batch size, retrieves items in 186 | batches allowing to retrieve more items than the limit. 187 | :param bool download_attachments: whether or not to download attachments 188 | :return: list of messages 189 | :rtype: list[Message] or Pagination 190 | """ 191 | 192 | if self.root: 193 | url = self.build_url(self._endpoints.get('root_messages')) 194 | else: 195 | url = self.build_url(self._endpoints.get('folder_messages').format( 196 | id=self.folder_id)) 197 | 198 | if not batch and (limit is None or limit > self.protocol.max_top_value): 199 | batch = self.protocol.max_top_value 200 | 201 | params = {'$top': batch if batch else limit} 202 | 203 | if order_by: 204 | params['$orderby'] = order_by 205 | 206 | if query: 207 | if isinstance(query, str): 208 | params['$filter'] = query 209 | else: 210 | params.update(query.as_params()) 211 | 212 | response = self.con.get(url, params=params) 213 | if not response: 214 | return iter(()) 215 | 216 | data = response.json() 217 | 218 | # Everything received from cloud must be passed as self._cloud_data_key 219 | messages = (self.message_constructor( 220 | parent=self, 221 | download_attachments=download_attachments, 222 | **{self._cloud_data_key: message}) 223 | for message in data.get('value', [])) 224 | 225 | next_link = data.get(NEXT_LINK_KEYWORD, None) 226 | if batch and next_link: 227 | return Pagination(parent=self, data=messages, 228 | constructor=self.message_constructor, 229 | next_link=next_link, limit=limit, 230 | download_attachments=download_attachments) 231 | else: 232 | return messages 233 | 234 | def create_child_folder(self, folder_name): 235 | """ Creates a new child folder under this folder 236 | 237 | :param str folder_name: name of the folder to add 238 | :return: newly created folder 239 | :rtype: mailbox.Folder or None 240 | """ 241 | if not folder_name: 242 | return None 243 | 244 | if self.root: 245 | url = self.build_url(self._endpoints.get('root_folders')) 246 | else: 247 | url = self.build_url( 248 | self._endpoints.get('child_folders').format(id=self.folder_id)) 249 | 250 | response = self.con.post(url, 251 | data={self._cc('displayName'): folder_name}) 252 | if not response: 253 | return None 254 | 255 | folder = response.json() 256 | 257 | self_class = getattr(self, 'folder_constructor', type(self)) 258 | # Everything received from cloud must be passed as self._cloud_data_key 259 | return self_class(parent=self, **{self._cloud_data_key: folder}) 260 | 261 | def get_folder(self, *, folder_id=None, folder_name=None): 262 | """ Get a folder by it's id or name 263 | 264 | :param str folder_id: the folder_id to be retrieved. 265 | Can be any folder Id (child or not) 266 | :param str folder_name: the folder name to be retrieved. 267 | Must be a child of this folder. 268 | :return: a single folder 269 | :rtype: mailbox.Folder or None 270 | """ 271 | if folder_id and folder_name: 272 | raise RuntimeError('Provide only one of the options') 273 | 274 | if not folder_id and not folder_name: 275 | raise RuntimeError('Provide one of the options') 276 | 277 | if folder_id: 278 | # get folder by it's id, independent of the parent of this folder_id 279 | url = self.build_url( 280 | self._endpoints.get('get_folder').format(id=folder_id)) 281 | params = None 282 | else: 283 | # get folder by name. Only looks up in child folders. 284 | if self.root: 285 | url = self.build_url(self._endpoints.get('root_folders')) 286 | else: 287 | url = self.build_url( 288 | self._endpoints.get('child_folders').format( 289 | id=self.folder_id)) 290 | params = {'$filter': "{} eq '{}'".format(self._cc('displayName'), 291 | folder_name), '$top': 1} 292 | 293 | response = self.con.get(url, params=params) 294 | if not response: 295 | return None 296 | 297 | if folder_id: 298 | folder = response.json() 299 | else: 300 | folder = response.json().get('value') 301 | folder = folder[0] if folder else None 302 | if folder is None: 303 | return None 304 | 305 | self_class = getattr(self, 'folder_constructor', type(self)) 306 | # Everything received from cloud must be passed as self._cloud_data_key 307 | # We don't pass parent, as this folder may not be a child of self. 308 | return self_class(con=self.con, protocol=self.protocol, 309 | main_resource=self.main_resource, 310 | **{self._cloud_data_key: folder}) 311 | 312 | def refresh_folder(self, update_parent_if_changed=False): 313 | """ Re-download folder data 314 | Inbox Folder will be unable to download its own data (no folder_id) 315 | 316 | :param bool update_parent_if_changed: updates self.parent with new 317 | parent Folder if changed 318 | :return: Refreshed or Not 319 | :rtype: bool 320 | """ 321 | folder_id = getattr(self, 'folder_id', None) 322 | if self.root or folder_id is None: 323 | return False 324 | 325 | folder = self.get_folder(folder_id=folder_id) 326 | if folder is None: 327 | return False 328 | 329 | self.name = folder.name 330 | if folder.parent_id and self.parent_id: 331 | if folder.parent_id != self.parent_id: 332 | self.parent_id = folder.parent_id 333 | self.parent = (self.get_parent_folder() 334 | if update_parent_if_changed else None) 335 | self.child_folders_count = folder.child_folders_count 336 | self.unread_items_count = folder.unread_items_count 337 | self.total_items_count = folder.total_items_count 338 | self.updated_at = folder.updated_at 339 | 340 | return True 341 | 342 | def get_parent_folder(self): 343 | """ Get the parent folder from attribute self.parent or 344 | getting it from the cloud 345 | 346 | :return: Parent Folder 347 | :rtype: mailbox.Folder or None 348 | """ 349 | if self.root: 350 | return None 351 | if self.parent: 352 | return self.parent 353 | 354 | if self.parent_id: 355 | self.parent = self.get_folder(folder_id=self.parent_id) 356 | return self.parent 357 | 358 | def update_folder_name(self, name, update_folder_data=True): 359 | """ Change this folder name 360 | 361 | :param str name: new name to change to 362 | :param bool update_folder_data: whether or not to re-fetch the data 363 | :return: Updated or Not 364 | :rtype: bool 365 | """ 366 | if self.root: 367 | return False 368 | if not name: 369 | return False 370 | 371 | url = self.build_url( 372 | self._endpoints.get('get_folder').format(id=self.folder_id)) 373 | 374 | response = self.con.patch(url, data={self._cc('displayName'): name}) 375 | if not response: 376 | return False 377 | 378 | self.name = name 379 | if not update_folder_data: 380 | return True 381 | 382 | folder = response.json() 383 | 384 | self.name = folder.get(self._cc('displayName'), '') 385 | self.parent_id = folder.get(self._cc('parentFolderId'), None) 386 | self.child_folders_count = folder.get(self._cc('childFolderCount'), 0) 387 | self.unread_items_count = folder.get(self._cc('unreadItemCount'), 0) 388 | self.total_items_count = folder.get(self._cc('totalItemCount'), 0) 389 | self.updated_at = dt.datetime.now() 390 | 391 | return True 392 | 393 | def delete(self): 394 | """ Deletes this folder 395 | 396 | :return: Deleted or Not 397 | :rtype: bool 398 | """ 399 | 400 | if self.root or not self.folder_id: 401 | return False 402 | 403 | url = self.build_url( 404 | self._endpoints.get('get_folder').format(id=self.folder_id)) 405 | 406 | response = self.con.delete(url) 407 | if not response: 408 | return False 409 | 410 | self.folder_id = None 411 | return True 412 | 413 | def copy_folder(self, to_folder): 414 | """ Copy this folder and it's contents to into another folder 415 | 416 | :param to_folder: the destination Folder/folder_id to copy into 417 | :type to_folder: mailbox.Folder or str 418 | :return: The new folder after copying 419 | :rtype: mailbox.Folder or None 420 | """ 421 | to_folder_id = to_folder.folder_id if isinstance(to_folder, 422 | Folder) else to_folder 423 | 424 | if self.root or not self.folder_id or not to_folder_id: 425 | return None 426 | 427 | url = self.build_url( 428 | self._endpoints.get('copy_folder').format(id=self.folder_id)) 429 | 430 | response = self.con.post(url, 431 | data={self._cc('destinationId'): to_folder_id}) 432 | if not response: 433 | return None 434 | 435 | folder = response.json() 436 | 437 | self_class = getattr(self, 'folder_constructor', type(self)) 438 | # Everything received from cloud must be passed as self._cloud_data_key 439 | return self_class(con=self.con, main_resource=self.main_resource, 440 | **{self._cloud_data_key: folder}) 441 | 442 | def move_folder(self, to_folder, *, update_parent_if_changed=True): 443 | """ Move this folder to another folder 444 | 445 | :param to_folder: the destination Folder/folder_id to move into 446 | :type to_folder: mailbox.Folder or str 447 | :param bool update_parent_if_changed: updates self.parent with the 448 | new parent Folder if changed 449 | :return: The new folder after copying 450 | :rtype: mailbox.Folder or None 451 | """ 452 | to_folder_id = to_folder.folder_id if isinstance(to_folder, 453 | Folder) else to_folder 454 | 455 | if self.root or not self.folder_id or not to_folder_id: 456 | return False 457 | 458 | url = self.build_url( 459 | self._endpoints.get('move_folder').format(id=self.folder_id)) 460 | 461 | response = self.con.post(url, 462 | data={self._cc('destinationId'): to_folder_id}) 463 | if not response: 464 | return False 465 | 466 | folder = response.json() 467 | 468 | parent_id = folder.get(self._cc('parentFolderId'), None) 469 | 470 | if parent_id and self.parent_id: 471 | if parent_id != self.parent_id: 472 | self.parent_id = parent_id 473 | self.parent = (self.get_parent_folder() 474 | if update_parent_if_changed else None) 475 | 476 | return True 477 | 478 | def new_message(self): 479 | """ Creates a new draft message under this folder 480 | 481 | :return: new Message 482 | :rtype: Message 483 | """ 484 | 485 | draft_message = self.message_constructor(parent=self, is_draft=True) 486 | 487 | if self.root: 488 | draft_message.folder_id = OutlookWellKnowFolderNames.DRAFTS.value 489 | else: 490 | draft_message.folder_id = self.folder_id 491 | 492 | return draft_message 493 | 494 | def delete_message(self, message): 495 | """ Deletes a stored message 496 | 497 | :param message: message/message_id to delete 498 | :type message: Message or str 499 | :return: Success / Failure 500 | :rtype: bool 501 | """ 502 | 503 | message_id = message.object_id if isinstance(message, 504 | Message) else message 505 | 506 | if message_id is None: 507 | raise RuntimeError('Provide a valid Message or a message id') 508 | 509 | url = self.build_url( 510 | self._endpoints.get('message').format(id=message_id)) 511 | 512 | response = self.con.delete(url) 513 | 514 | return bool(response) 515 | 516 | 517 | class MailBox(Folder): 518 | folder_constructor = Folder 519 | 520 | def __init__(self, *, parent=None, con=None, **kwargs): 521 | super().__init__(parent=parent, con=con, root=True, **kwargs) 522 | 523 | def inbox_folder(self): 524 | """ Shortcut to get Inbox Folder instance 525 | 526 | :rtype: mailbox.Folder 527 | """ 528 | return self.folder_constructor(parent=self, name='Inbox', 529 | folder_id=OutlookWellKnowFolderNames 530 | .INBOX.value) 531 | 532 | def junk_folder(self): 533 | """ Shortcut to get Junk Folder instance 534 | 535 | :rtype: mailbox.Folder 536 | """ 537 | return self.folder_constructor(parent=self, name='Junk', 538 | folder_id=OutlookWellKnowFolderNames 539 | .JUNK.value) 540 | 541 | def deleted_folder(self): 542 | """ Shortcut to get DeletedItems Folder instance 543 | 544 | :rtype: mailbox.Folder 545 | """ 546 | return self.folder_constructor(parent=self, name='DeletedItems', 547 | folder_id=OutlookWellKnowFolderNames 548 | .DELETED.value) 549 | 550 | def drafts_folder(self): 551 | """ Shortcut to get Drafts Folder instance 552 | 553 | :rtype: mailbox.Folder 554 | """ 555 | return self.folder_constructor(parent=self, name='Drafts', 556 | folder_id=OutlookWellKnowFolderNames 557 | .DRAFTS.value) 558 | 559 | def sent_folder(self): 560 | """ Shortcut to get SentItems Folder instance 561 | 562 | :rtype: mailbox.Folder 563 | """ 564 | return self.folder_constructor(parent=self, name='SentItems', 565 | folder_id=OutlookWellKnowFolderNames 566 | .SENT.value) 567 | 568 | def outbox_folder(self): 569 | """ Shortcut to get Outbox Folder instance 570 | 571 | :rtype: mailbox.Folder 572 | """ 573 | return self.folder_constructor(parent=self, name='Outbox', 574 | folder_id=OutlookWellKnowFolderNames 575 | .OUTBOX.value) 576 | 577 | def archive_folder(self): 578 | """ Shortcut to get Archive Folder instance 579 | 580 | :rtype: mailbox.Folder 581 | """ 582 | return self.folder_constructor(parent=self, name='Archive', 583 | folder_id=OutlookWellKnowFolderNames 584 | .ARCHIVE.value) 585 | -------------------------------------------------------------------------------- /o365_Test_Project/O365/planner.py: -------------------------------------------------------------------------------- 1 | #import logging 2 | 3 | from dateutil.parser import parse 4 | from .utils import ApiComponent 5 | 6 | #log = logging.getLogger(__name__) 7 | 8 | 9 | class Task(ApiComponent): 10 | """ A Microsoft Planner task """ 11 | 12 | _endpoints = {} 13 | 14 | def __init__(self, *, parent=None, con=None, **kwargs): 15 | """ A Microsoft planner task 16 | 17 | :param parent: parent object 18 | :type parent: Planner 19 | :param Connection con: connection to use if no parent specified 20 | :param Protocol protocol: protocol to use if no parent specified 21 | (kwargs) 22 | :param str main_resource: use this resource instead of parent resource 23 | (kwargs) 24 | """ 25 | if parent and con: 26 | raise ValueError('Need a parent or a connection but not both') 27 | self.con = parent.con if parent else con 28 | 29 | cloud_data = kwargs.get(self._cloud_data_key, {}) 30 | 31 | self.object_id = cloud_data.get('id') 32 | 33 | # Choose the main_resource passed in kwargs over parent main_resource 34 | main_resource = kwargs.pop('main_resource', None) or ( 35 | getattr(parent, 'main_resource', None) if parent else None) 36 | 37 | main_resource = '{}{}'.format(main_resource, '') 38 | 39 | super().__init__( 40 | protocol=parent.protocol if parent else kwargs.get('protocol'), 41 | main_resource=main_resource) 42 | 43 | self.plan_id = cloud_data.get('plan_id') 44 | self.bucket_id = cloud_data.get('bucketId') 45 | self.title = cloud_data.get(self._cc('title'), '') 46 | self.order_hint = cloud_data.get(self._cc('orderHint'), '') 47 | self.assignee_priority = cloud_data.get(self._cc('assigneePriority'), '') 48 | self.percent_complete = cloud_data.get(self._cc('percentComplete'), '') 49 | self.title = cloud_data.get(self._cc('title'), '') 50 | self.has_description = cloud_data.get(self._cc('hasDescription'), '') 51 | created = cloud_data.get(self._cc('createdDateTime'), None) 52 | due_date = cloud_data.get(self._cc('dueDateTime'), None) 53 | start_date = cloud_data.get(self._cc('startDateTime'), None) 54 | completed_date = cloud_data.get(self._cc('completedDateTime'), None) 55 | local_tz = self.protocol.timezone 56 | self.start_date = parse(start_date).astimezone(local_tz) if start_date else None 57 | self.created_date = parse(created).astimezone(local_tz) if created else None 58 | self.due_date = parse(due_date).astimezone(local_tz) if due_date else None 59 | self.completed_date = parse(completed_date).astimezone(local_tz) if completed_date else None 60 | self.preview_type = cloud_data.get(self._cc('previewType'), None) 61 | self.reference_count = cloud_data.get(self._cc('referenceCount'), None) 62 | self.checklist_item_count = cloud_data.get(self._cc('checklistItemCount'), None) 63 | self.active_checklist_item_count = cloud_data.get(self._cc('activeChecklistItemCount'), None) 64 | self.conversation_thread_id = cloud_data.get(self._cc('conversationThreadId'), None) 65 | 66 | def __str__(self): 67 | return self.__repr__() 68 | 69 | def __repr__(self): 70 | return 'Task: {}'.format(self.title) 71 | 72 | def __eq__(self, other): 73 | return self.object_id == other.object_id 74 | 75 | 76 | class Planner(ApiComponent): 77 | """ A microsoft planner class 78 | In order to use the API following permissions are required. 79 | Delegated (work or school account) - Group.Read.All, Group.ReadWrite.All 80 | """ 81 | 82 | _endpoints = { 83 | 'get_my_tasks': '/me/planner/tasks', 84 | } 85 | task_constructor = Task 86 | 87 | def __init__(self, *, parent=None, con=None, **kwargs): 88 | """ A Planner object 89 | 90 | :param parent: parent object 91 | :type parent: Account 92 | :param Connection con: connection to use if no parent specified 93 | :param Protocol protocol: protocol to use if no parent specified 94 | (kwargs) 95 | :param str main_resource: use this resource instead of parent resource 96 | (kwargs) 97 | """ 98 | if parent and con: 99 | raise ValueError('Need a parent or a connection but not both') 100 | self.con = parent.con if parent else con 101 | 102 | # Choose the main_resource passed in kwargs over the host_name 103 | main_resource = kwargs.pop('main_resource', 104 | '') # defaults to blank resource 105 | super().__init__( 106 | protocol=parent.protocol if parent else kwargs.get('protocol'), 107 | main_resource=main_resource) 108 | 109 | def __str__(self): 110 | return self.__repr__() 111 | 112 | def __repr__(self): 113 | return 'Microsoft Planner' 114 | 115 | def get_my_tasks(self, *args): 116 | """ Returns a list of open planner tasks assigned to me 117 | 118 | :rtype: tasks 119 | """ 120 | 121 | url = self.build_url(self._endpoints.get('get_my_tasks')) 122 | 123 | response = self.con.get(url) 124 | 125 | if not response: 126 | return None 127 | 128 | data = response.json() 129 | 130 | return [ 131 | self.task_constructor(parent=self, **{self._cloud_data_key: site}) 132 | for site in data.get('value', [])] 133 | -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .attachment import BaseAttachments, BaseAttachment, AttachableMixin 2 | from .utils import ApiComponent, OutlookWellKnowFolderNames 3 | from .utils import CaseEnum, ImportanceLevel, TrackerSet 4 | from .utils import Recipient, Recipients, HandleRecipientsMixin 5 | from .utils import NEXT_LINK_KEYWORD, ME_RESOURCE, USERS_RESOURCE 6 | from .utils import OneDriveWellKnowFolderNames, Pagination, Query 7 | from .token import BaseTokenBackend, Token, FileSystemTokenBackend, FirestoreBackend, AWSS3Backend, AWSSecretsBackend 8 | from .windows_tz import IANA_TO_WIN, WIN_TO_IANA 9 | from . import consent -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/utils/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/__pycache__/attachment.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/utils/__pycache__/attachment.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/__pycache__/consent.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/utils/__pycache__/consent.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/__pycache__/decorators.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/utils/__pycache__/decorators.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/__pycache__/token.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/utils/__pycache__/token.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/__pycache__/utils.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/utils/__pycache__/utils.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/__pycache__/windows_tz.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/o365_Test_Project/O365/utils/__pycache__/windows_tz.cpython-39.pyc -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/consent.py: -------------------------------------------------------------------------------- 1 | def consent_input_token(consent_url): 2 | print('Visit the following url to give consent:') 3 | print(consent_url) 4 | 5 | return input('Paste the authenticated url here:\n') -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/decorators.py: -------------------------------------------------------------------------------- 1 | #import logging 2 | from functools import wraps 3 | 4 | #log = logging.getLogger(__name__) 5 | 6 | 7 | def deprecated(version, *replacement): 8 | """ Decorator to mark a specified function as deprecated 9 | 10 | :param version: version in which it is deprecated 11 | :param replacement: replacement functions to use 12 | """ 13 | 14 | def deprecated_wrapper(func): 15 | replacement_message = 'Use {} instead'.format(', '.join( 16 | ["'{}'".format(_get_func_fq_name(x)) 17 | for x in replacement])) 18 | log_message = ("'{}' is deprecated, {}" 19 | "".format(_get_func_fq_name(func), replacement_message)) 20 | # func.__doc__ = replacement[0].__doc__ 21 | 22 | func_path = _get_func_path(func) 23 | doc_replacement = [] 24 | for x in replacement: 25 | if func_path == _get_func_path(x): 26 | doc_replacement.append(':func:`{}`'.format(_func_name(x))) 27 | else: 28 | doc_replacement.append( 29 | ':func:`{}`'.format(_get_func_fq_name(x))) 30 | 31 | func.__doc__ = """ 32 | .. deprecated:: {} 33 | Use {} instead 34 | 35 | {} 36 | """.format(version, 37 | ', '.join(doc_replacement), 38 | func.__doc__ if func.__doc__ else '') 39 | 40 | @wraps(func) 41 | def wrapper(*args, **kwargs): 42 | #log.warning(log_message) 43 | return func(*args, **kwargs) 44 | 45 | return wrapper 46 | 47 | return deprecated_wrapper 48 | 49 | 50 | def _func_name(func): 51 | if isinstance(func, property): 52 | func = func.fget 53 | return func.__name__ 54 | 55 | 56 | def _get_func_path(func): 57 | if isinstance(func, property): 58 | func = func.fget 59 | full_path = "{}.".format(func.__module__) 60 | if callable(func): 61 | try: 62 | temp = func.__qualname__.split('.', 1)[0].rsplit('.', 1)[0] 63 | full_path += "{}.".format(temp) 64 | except AttributeError as _: 65 | try: 66 | # noinspection PyUnresolvedReferences 67 | temp = func.im_class 68 | full_path += "{}.".format(temp) 69 | except AttributeError as _: 70 | pass 71 | 72 | return full_path 73 | 74 | 75 | def _get_func_fq_name(func): 76 | if isinstance(func, property): 77 | func = func.fget 78 | full_path = _get_func_path(func) 79 | full_path += func.__name__ 80 | return full_path 81 | 82 | 83 | def fluent(func): 84 | func.__doc__ = """{} 85 | .. note:: This method is part of fluent api and can be chained 86 | """.format(func.__doc__ if func.__doc__ else '') 87 | 88 | @wraps(func) 89 | def inner(self, *args, **kwargs): 90 | return func(self, *args, **kwargs) 91 | 92 | return inner 93 | 94 | 95 | def action(func): 96 | func.__doc__ = """{} 97 | .. note:: The success/failure of this action can be obtained 98 | from **success** and **error_message** attributes after 99 | executing this function 100 | 101 | Example: 102 | .. code-block:: python 103 | 104 | my_obj.one().two().finish() 105 | if not my_obj.is_success: 106 | print(my_obj.error_message) 107 | 108 | this will return success/failure of **finish** action 109 | """.format(func.__doc__ if func.__doc__ else '') 110 | 111 | @wraps(func) 112 | def inner(self, *args, **kwargs): 113 | obj = self.__class__.__new__(self.__class__) 114 | obj.__dict__ = self.__dict__.copy() 115 | func(obj, *args, **kwargs) 116 | return obj 117 | 118 | return inner 119 | -------------------------------------------------------------------------------- /o365_Test_Project/O365/utils/token.py: -------------------------------------------------------------------------------- 1 | ##import logging 2 | import json 3 | import datetime as dt 4 | from pathlib import Path 5 | from abc import ABC, abstractmethod 6 | import os 7 | 8 | ##log = logging.getLogger(__name__) 9 | 10 | EXPIRES_ON_THRESHOLD = 1 * 60 # 1 minute 11 | 12 | 13 | class Token(dict): 14 | """ A dict subclass with extra methods to resemble a token """ 15 | 16 | @property 17 | def is_long_lived(self): 18 | """ 19 | Checks whether this token has a refresh token 20 | :return bool: True if has a refresh_token 21 | """ 22 | return 'refresh_token' in self 23 | 24 | @property 25 | def is_expired(self): 26 | """ 27 | Checks whether this token is expired 28 | :return bool: True if the token is expired, False otherwise 29 | """ 30 | return dt.datetime.now() > self.expiration_datetime 31 | 32 | @property 33 | def expiration_datetime(self): 34 | """ 35 | Returns the expiration datetime 36 | :return datetime: The datetime this token expires 37 | """ 38 | access_expires_at = self.access_expiration_datetime 39 | expires_on = access_expires_at - dt.timedelta(seconds=EXPIRES_ON_THRESHOLD) 40 | if self.is_long_lived: 41 | expires_on = expires_on + dt.timedelta(days=90) 42 | return expires_on 43 | 44 | @property 45 | def access_expiration_datetime(self): 46 | """ 47 | Returns the token's access expiration datetime 48 | :return datetime: The datetime the token's access expires 49 | """ 50 | expires_at = self.get('expires_at') 51 | if expires_at: 52 | return dt.datetime.fromtimestamp(expires_at) 53 | else: 54 | # consider the token expired, add 10 second buffer to current dt 55 | return dt.datetime.now() - dt.timedelta(seconds=10) 56 | 57 | @property 58 | def is_access_expired(self): 59 | """ 60 | Returns whether or not the token's access is expired. 61 | :return bool: True if the token's access is expired, False otherwise 62 | """ 63 | return dt.datetime.now() > self.access_expiration_datetime 64 | 65 | 66 | class BaseTokenBackend(ABC): 67 | """ A base token storage class """ 68 | 69 | serializer = json # The default serializer is json 70 | token_constructor = Token # the default token constructor 71 | 72 | def __init__(self): 73 | self._token = None 74 | 75 | @property 76 | def token(self): 77 | """ The stored Token dict """ 78 | return self._token 79 | 80 | @token.setter 81 | def token(self, value): 82 | """ Setter to convert any token dict into Token instance """ 83 | if value and not isinstance(value, Token): 84 | value = Token(value) 85 | self._token = value 86 | 87 | @abstractmethod 88 | def load_token(self): 89 | """ Abstract method that will retrieve the oauth token """ 90 | raise NotImplementedError 91 | 92 | def get_token(self): 93 | """ Loads the token, stores it in the token property and returns it""" 94 | self.token = self.load_token() # store the token in the 'token' property 95 | return self.token 96 | 97 | @abstractmethod 98 | def save_token(self): 99 | """ Abstract method that will save the oauth token """ 100 | raise NotImplementedError 101 | 102 | def delete_token(self): 103 | """ Optional Abstract method to delete the token """ 104 | raise NotImplementedError 105 | 106 | def check_token(self): 107 | """ Optional Abstract method to check for the token existence """ 108 | raise NotImplementedError 109 | 110 | def should_refresh_token(self, con=None): 111 | """ 112 | This method is intended to be implemented for environments 113 | where multiple Connection instances are running on paralel. 114 | 115 | This method should check if it's time to refresh the token or not. 116 | The chosen backend can store a flag somewhere to answer this question. 117 | This can avoid race conditions between different instances trying to 118 | refresh the token at once, when only one should make the refresh. 119 | 120 | > This is an example of how to achieve this: 121 | > 1) Along with the token store a Flag 122 | > 2) The first to see the Flag as True must transacionally update it 123 | > to False. This method then returns True and therefore the 124 | > connection will refresh the token. 125 | > 3) The save_token method should be rewrited to also update the flag 126 | > back to True always. 127 | > 4) Meanwhile between steps 2 and 3, any other token backend checking 128 | > for this method should get the flag with a False value. 129 | > This method should then wait and check again the flag. 130 | > This can be implemented as a call with an incremental backoff 131 | > factor to avoid too many calls to the database. 132 | > At a given point in time, the flag will return True. 133 | > Then this method should load the token and finally return False 134 | > signaling there is no need to refresh the token. 135 | 136 | If this returns True, then the Connection will refresh the token. 137 | If this returns False, then the Connection will NOT refresh the token. 138 | If this returns None, then this method already executed the refresh and therefore 139 | the Connection does not have to. 140 | 141 | By default this always returns True 142 | 143 | There is an example of this in the examples folder. 144 | 145 | :param Connection con: the connection that calls this method. This 146 | is passed because maybe the locking mechanism needs to refresh the 147 | token within the lock applied in this method. 148 | :rtype: bool or None 149 | :return: True if the Connection can refresh the token 150 | False if the Connection should not refresh the token 151 | None if the token was refreshed and therefore the 152 | Connection should do nothing. 153 | """ 154 | return True 155 | 156 | 157 | class FileSystemTokenBackend(BaseTokenBackend): 158 | """ A token backend based on files on the filesystem """ 159 | 160 | def __init__(self, token_path=None, token_filename=None): 161 | """ 162 | Init Backend 163 | :param str or Path token_path: the path where to store the token 164 | :param str token_filename: the name of the token file 165 | """ 166 | super().__init__() 167 | if not isinstance(token_path, Path): 168 | token_path = Path(token_path) if token_path else Path() 169 | 170 | if token_path.is_file(): 171 | self.token_path = token_path 172 | else: 173 | token_filename = token_filename or 'o365_token.txt' 174 | self.token_path = token_path / token_filename 175 | 176 | def __repr__(self): 177 | return str(self.token_path) 178 | 179 | def load_token(self): 180 | """ 181 | Retrieves the token from the File System 182 | :return dict or None: The token if exists, None otherwise 183 | """ 184 | token = None 185 | if self.token_path.exists(): 186 | with self.token_path.open('r') as token_file: 187 | token = self.token_constructor(self.serializer.load(token_file)) 188 | return token 189 | 190 | def save_token(self): 191 | """ 192 | Saves the token dict in the specified file 193 | :return bool: Success / Failure 194 | """ 195 | if self.token is None: 196 | raise ValueError('You have to set the "token" first.') 197 | 198 | try: 199 | if not self.token_path.parent.exists(): 200 | self.token_path.parent.mkdir(parents=True) 201 | except Exception as e: 202 | ##log.error('Token could not be saved: {}'.format(str(e))) 203 | return False 204 | 205 | with self.token_path.open('w') as token_file: 206 | # 'indent = True' will make the file human readable 207 | self.serializer.dump(self.token, token_file, indent=True) 208 | 209 | return True 210 | 211 | def delete_token(self): 212 | """ 213 | Deletes the token file 214 | :return bool: Success / Failure 215 | """ 216 | if self.token_path.exists(): 217 | self.token_path.unlink() 218 | return True 219 | return False 220 | 221 | def check_token(self): 222 | """ 223 | Cheks if the token exists in the filesystem 224 | :return bool: True if exists, False otherwise 225 | """ 226 | return self.token_path.exists() 227 | 228 | 229 | class FirestoreBackend(BaseTokenBackend): 230 | """ A Google Firestore database backend to store tokens """ 231 | 232 | def __init__(self, client, collection, doc_id, field_name='token'): 233 | """ 234 | Init Backend 235 | :param firestore.Client client: the firestore Client instance 236 | :param str collection: the firestore collection where to store tokens (can be a field_path) 237 | :param str doc_id: # the key of the token document. Must be unique per-case. 238 | :param str field_name: the name of the field that stores the token in the document 239 | """ 240 | super().__init__() 241 | self.client = client 242 | self.collection = collection 243 | self.doc_id = doc_id 244 | self.doc_ref = client.collection(collection).document(doc_id) 245 | self.field_name = field_name 246 | 247 | def __repr__(self): 248 | return 'Collection: {}. Doc Id: {}'.format(self.collection, self.doc_id) 249 | 250 | def load_token(self): 251 | """ 252 | Retrieves the token from the store 253 | :return dict or None: The token if exists, None otherwise 254 | """ 255 | token = None 256 | try: 257 | doc = self.doc_ref.get() 258 | except Exception as e: 259 | ##log.error('Token (collection: {}, doc_id: {}) ' 260 | #'could not be retrieved from the backend: {}' 261 | #.format(self.collection, self.doc_id, str(e))) 262 | doc = None 263 | if doc and doc.exists: 264 | token_str = doc.get(self.field_name) 265 | if token_str: 266 | token = self.token_constructor(self.serializer.loads(token_str)) 267 | return token 268 | 269 | def save_token(self): 270 | """ 271 | Saves the token dict in the store 272 | :return bool: Success / Failure 273 | """ 274 | if self.token is None: 275 | raise ValueError('You have to set the "token" first.') 276 | 277 | try: 278 | # set token will overwrite previous data 279 | self.doc_ref.set({ 280 | self.field_name: self.serializer.dumps(self.token) 281 | }) 282 | except Exception as e: 283 | ##log.error('Token could not be saved: {}'.format(str(e))) 284 | return False 285 | 286 | return True 287 | 288 | def delete_token(self): 289 | """ 290 | Deletes the token from the store 291 | :return bool: Success / Failure 292 | """ 293 | try: 294 | self.doc_ref.delete() 295 | except Exception as e: 296 | ##log.error('Could not delete the token (key: {}): {}'.format(self.doc_id, str(e))) 297 | return False 298 | return True 299 | 300 | def check_token(self): 301 | """ 302 | Checks if the token exists 303 | :return bool: True if it exists on the store 304 | """ 305 | try: 306 | doc = self.doc_ref.get() 307 | except Exception as e: 308 | ##log.error('Token (collection: {}, doc_id: {}) ' 309 | #'could not be retrieved from the backend: {}' 310 | #.format(self.collection, self.doc_id, str(e))) 311 | doc = None 312 | return doc and doc.exists 313 | 314 | 315 | class AWSS3Backend(BaseTokenBackend): 316 | """ An AWS S3 backend to store tokens """ 317 | 318 | def __init__(self, bucket_name, filename): 319 | """ 320 | Init Backend 321 | :param str file_name: Name of the S3 bucket 322 | :param str file_name: Name of the file 323 | """ 324 | try: 325 | import boto3 326 | except ModuleNotFoundError as e: 327 | raise Exception('Please install the boto3 package to use this token backend.') from e 328 | super().__init__() 329 | self.bucket_name = bucket_name 330 | self.filename = filename 331 | self._client = boto3.client('s3') 332 | 333 | def __repr__(self): 334 | return "AWSS3Backend('{}', '{}')".format(self.bucket_name, self.filename) 335 | 336 | def load_token(self): 337 | """ 338 | Retrieves the token from the store 339 | :return dict or None: The token if exists, None otherwise 340 | """ 341 | token = None 342 | try: 343 | token_object = self._client.get_object(Bucket=self.bucket_name, Key=self.filename) 344 | token = self.token_constructor(self.serializer.loads(token_object['Body'].read())) 345 | except Exception as e: 346 | pass 347 | ##log.error("Token ({}) could not be retrieved from the backend: {}".format(self.filename, e)) 348 | 349 | return token 350 | 351 | def save_token(self): 352 | """ 353 | Saves the token dict in the store 354 | :return bool: Success / Failure 355 | """ 356 | if self.token is None: 357 | raise ValueError('You have to set the "token" first.') 358 | 359 | token_str = str.encode(self.serializer.dumps(self.token)) 360 | if self.check_token(): # file already exists 361 | try: 362 | _ = self._client.put_object( 363 | Bucket=self.bucket_name, 364 | Key=self.filename, 365 | Body=token_str 366 | ) 367 | except Exception as e: 368 | ##log.error("Token file could not be saved: {}".format(e)) 369 | return False 370 | else: # create a new token file 371 | try: 372 | r = self._client.put_object( 373 | ACL='private', 374 | Bucket=self.bucket_name, 375 | Key=self.filename, 376 | Body=token_str, 377 | ContentType='text/plain' 378 | ) 379 | except Exception as e: 380 | ##log.error("Token file could not be created: {}".format(e)) 381 | return False 382 | 383 | return True 384 | 385 | def delete_token(self): 386 | """ 387 | Deletes the token from the store 388 | :return bool: Success / Failure 389 | """ 390 | try: 391 | r = self._client.delete_object(Bucket=self.bucket_name, Key=self.filename) 392 | except Exception as e: 393 | ##log.error("Token file could not be deleted: {}".format(e)) 394 | return False 395 | else: 396 | ##log.warning("Deleted token file {} in bucket {}.".format(self.filename, self.bucket_name)) 397 | return True 398 | 399 | def check_token(self): 400 | """ 401 | Checks if the token exists 402 | :return bool: True if it exists on the store 403 | """ 404 | try: 405 | _ = self._client.head_object(Bucket=self.bucket_name, Key=self.filename) 406 | except: 407 | return False 408 | else: 409 | return True 410 | 411 | 412 | class AWSSecretsBackend(BaseTokenBackend): 413 | """ An AWS Secrets Manager backend to store tokens """ 414 | 415 | def __init__(self, secret_name, region_name): 416 | """ 417 | Init Backend 418 | :param str secret_name: Name of the secret stored in Secrets Manager 419 | :param str region_name: AWS region hosting the secret (for example, 'us-east-2') 420 | """ 421 | try: 422 | import boto3 423 | except ModuleNotFoundError as e: 424 | raise Exception('Please install the boto3 package to use this token backend.') from e 425 | super().__init__() 426 | self.secret_name = secret_name 427 | self.region_name = region_name 428 | self._client = boto3.client('secretsmanager', region_name=region_name) 429 | 430 | def __repr__(self): 431 | return "AWSSecretsBackend('{}', '{}')".format(self.secret_name, self.region_name) 432 | 433 | def load_token(self): 434 | """ 435 | Retrieves the token from the store 436 | :return dict or None: The token if exists, None otherwise 437 | """ 438 | token = None 439 | try: 440 | get_secret_value_response = self._client.get_secret_value(SecretId=self.secret_name) 441 | token_str = get_secret_value_response['SecretString'] 442 | token = self.token_constructor(self.serializer.loads(token_str)) 443 | except Exception as e: 444 | pass 445 | ##log.error("Token (secret: {}) could not be retrieved from the backend: {}".format(self.secret_name, e)) 446 | 447 | return token 448 | 449 | def save_token(self): 450 | """ 451 | Saves the token dict in the store 452 | :return bool: Success / Failure 453 | """ 454 | if self.token is None: 455 | raise ValueError('You have to set the "token" first.') 456 | 457 | if self.check_token(): # secret already exists 458 | try: 459 | _ = self._client.update_secret( 460 | SecretId=self.secret_name, 461 | SecretString=self.serializer.dumps(self.token) 462 | ) 463 | except Exception as e: 464 | ##log.error("Token secret could not be saved: {}".format(e)) 465 | return False 466 | else: # create a new secret 467 | try: 468 | r = self._client.create_secret( 469 | Name=self.secret_name, 470 | Description='Token generated by the O365 python package (https://pypi.org/project/O365/).', 471 | SecretString=self.serializer.dumps(self.token) 472 | ) 473 | except Exception as e: 474 | ##log.error("Token secret could not be created: {}".format(e)) 475 | return False 476 | 477 | return True 478 | 479 | def delete_token(self): 480 | """ 481 | Deletes the token from the store 482 | :return bool: Success / Failure 483 | """ 484 | try: 485 | r = self._client.delete_secret(SecretId=self.secret_name, ForceDeleteWithoutRecovery=True) 486 | except Exception as e: 487 | ##log.error("Token secret could not be deleted: {}".format(e)) 488 | return False 489 | else: 490 | ##log.warning("Deleted token secret {} ({}).".format(r['Name'], r['ARN'])) 491 | return True 492 | 493 | def check_token(self): 494 | """ 495 | Checks if the token exists 496 | :return bool: True if it exists on the store 497 | """ 498 | try: 499 | _ = self._client.describe_secret(SecretId=self.secret_name) 500 | except: 501 | return False 502 | else: 503 | return True 504 | -------------------------------------------------------------------------------- /o365_Test_Project/PythonO365.py: -------------------------------------------------------------------------------- 1 | #=================BiliBili日出东水=================== 2 | # o365日历获取测试Demo 3 | #---------------------------------------------------- 4 | 5 | from O365 import Account 6 | import datetime 7 | import time 8 | import os 9 | from collections import OrderedDict 10 | 11 | 12 | #这里填写客户端ID #API权限中的值(第一次生成时才能看到) 13 | credentials = ('13e82fa6-d204-4c2a-8670-f388c84277bc', 'aVk8Q~IlcKijyfbSp0dedNgMqm~Eiy_jlzOjDa97') 14 | 15 | #第一次运行,生成令牌时运行下面代码 16 | #---------------------------------------------------- 17 | #account = Account(credentials) 18 | #if account.authenticate(scopes=['basic', 'calendar_all']): 19 | # print('Authenticated! 验证成功!') 20 | #---------------------------------------------------- 21 | 22 | 23 | #如想查看所有event属性,可以查看源代码,在976行开始,搜索所有的 @property 24 | #https://github.com/O365/python-o365/blob/412d5b7dc521328ceb84a0086f1d89036bc09bd8/O365/calendar.py 25 | 26 | def GetO365(maxCount): 27 | account = Account(credentials) 28 | schedule = account.schedule() 29 | #查询从今天开始一个月内的日历 30 | #也可以指定 datetime(2022, 5, 30) 31 | now_time = datetime.datetime.now() 32 | end_time = datetime.timedelta(days =30) 33 | range_time = (now_time + end_time).strftime('%Y-%m-%d') 34 | 35 | q = schedule.new_query('start').greater_equal(now_time) 36 | q.chain('and').on_attribute('end').less_equal(range_time) 37 | 38 | getSchedule = schedule.get_events(query=q, include_recurring=True) 39 | 40 | scheduleDic = OrderedDict() 41 | scheduleCount = 0 42 | for event in getSchedule: 43 | #获取位置 44 | locationStr = str(event.location).split(",")[0].split(":")[1].replace("'","").replace(" ","") 45 | #获取时间 46 | dateTime = event.start 47 | #获取标题 48 | subjectStr = event.subject 49 | #获取正文 50 | bodyStr = str(event.body) 51 | startIndex = bodyStr.find("body") 52 | endIndex = bodyStr.find("/body") 53 | bodyProcessStr = bodyStr[startIndex+6:endIndex-1].replace("\n","") 54 | 55 | elemDic = OrderedDict() 56 | elemDic["location"] = locationStr 57 | elemDic["dateTime"] = dateTime 58 | elemDic["subjectStr"] = subjectStr 59 | elemDic["bodyStr"] = bodyProcessStr 60 | scheduleDic[scheduleCount] = elemDic 61 | scheduleCount+=1 62 | if scheduleCount >=maxCount: 63 | break 64 | return scheduleDic 65 | 66 | 67 | while True: 68 | scheduleDic = GetO365(5) 69 | print("\n查询到日程数量: "+str(len(scheduleDic))+"\n") 70 | 71 | for x in scheduleDic: 72 | print("位置: "+ scheduleDic[x]["location"]) 73 | t = scheduleDic[x]["dateTime"] 74 | print("时间: "+ str(t.month) +"月"+ str(t.day)+"日" + str(t.strftime('%H:%M'))) 75 | print("标题: "+ scheduleDic[x]["subjectStr"]) 76 | print("正文: "+ scheduleDic[x]["bodyStr"]) 77 | print("\n---------------------\n") 78 | print("等待10秒后刷新") 79 | print("=======================================\n\n") 80 | time.sleep(10) 81 | 82 | 83 | #新建日历事件 84 | #创建一个新事件 85 | #new_event = schedule.new_event() 86 | #new_event.subject = '这里写标题' 87 | #new_event.location = '位置' 88 | #时间 89 | #new_event.start = dt.datetime(2022, 4, 18, 19, 45) 90 | #设置重复 91 | #new_event.recurrence.set_daily(1, end=dt.datetime(2022, 4, 19)) 92 | #提前提醒时间 93 | #new_event.remind_before_minutes = 45 94 | #更新保存 95 | #new_event.save() 96 | 97 | #发送邮件 98 | #m = account.new_message() 99 | #m.to.add('XXXXXXX@qq.com') 100 | #m.subject = 'Testing!' 101 | #m.body = "这里写正文" 102 | #m.send() 103 | 104 | 105 | -------------------------------------------------------------------------------- /o365_Test_Project/PythonO365.pyproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Debug 4 | 2.0 5 | 5afc693d-46a5-4409-8c1c-94956134ca77 6 | . 7 | PythonO365.py 8 | 9 | 10 | . 11 | . 12 | PythonO365 13 | PythonO365 14 | 15 | 16 | true 17 | false 18 | 19 | 20 | true 21 | false 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /o365_Test_Project/PythonO365.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31205.134 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "PythonO365", "PythonO365.pyproj", "{5AFC693D-46A5-4409-8C1C-94956134CA77}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {5AFC693D-46A5-4409-8C1C-94956134CA77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {5AFC693D-46A5-4409-8C1C-94956134CA77}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ExtensibilityGlobals) = postSolution 21 | SolutionGuid = {BEE9C675-B212-4232-B351-0B900F97B26B} 22 | EndGlobalSection 23 | EndGlobal 24 | -------------------------------------------------------------------------------- /photo/Pla1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/Pla1.png -------------------------------------------------------------------------------- /photo/Pla2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/Pla2.png -------------------------------------------------------------------------------- /photo/Pla3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/Pla3.png -------------------------------------------------------------------------------- /photo/Pla4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/Pla4.png -------------------------------------------------------------------------------- /photo/demo0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/demo0.jpg -------------------------------------------------------------------------------- /photo/demo1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/demo1.jpg -------------------------------------------------------------------------------- /photo/demo2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/demo2.jpg -------------------------------------------------------------------------------- /photo/demo3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/demo3.jpg -------------------------------------------------------------------------------- /photo/demo4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/demo4.jpg -------------------------------------------------------------------------------- /photo/demo5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/demo5.jpg -------------------------------------------------------------------------------- /photo/demo6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/demo6.jpg -------------------------------------------------------------------------------- /photo/demo7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/photo/demo7.jpg -------------------------------------------------------------------------------- /pic/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/bg.png -------------------------------------------------------------------------------- /pic/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/icon.png -------------------------------------------------------------------------------- /pic/moisture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/moisture.png -------------------------------------------------------------------------------- /pic/temp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/temp.png -------------------------------------------------------------------------------- /pic/weatherType/中雨.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/中雨.bmp -------------------------------------------------------------------------------- /pic/weatherType/冻雨.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/冻雨.bmp -------------------------------------------------------------------------------- /pic/weatherType/多云.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/多云.bmp -------------------------------------------------------------------------------- /pic/weatherType/大雨.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/大雨.bmp -------------------------------------------------------------------------------- /pic/weatherType/小到中雨.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/小到中雨.bmp -------------------------------------------------------------------------------- /pic/weatherType/小雨.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/小雨.bmp -------------------------------------------------------------------------------- /pic/weatherType/无天气类型.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/无天气类型.bmp -------------------------------------------------------------------------------- /pic/weatherType/晴.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/晴.bmp -------------------------------------------------------------------------------- /pic/weatherType/暴雨.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/暴雨.bmp -------------------------------------------------------------------------------- /pic/weatherType/沙尘暴.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/沙尘暴.bmp -------------------------------------------------------------------------------- /pic/weatherType/阴.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/阴.bmp -------------------------------------------------------------------------------- /pic/weatherType/阵雨.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/阵雨.bmp -------------------------------------------------------------------------------- /pic/weatherType/阵雪.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/阵雪.bmp -------------------------------------------------------------------------------- /pic/weatherType/雨夹雪.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/雨夹雪.bmp -------------------------------------------------------------------------------- /pic/weatherType/雷阵雨.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/雷阵雨.bmp -------------------------------------------------------------------------------- /pic/weatherType/雷阵雨伴有冰雹.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/雷阵雨伴有冰雹.bmp -------------------------------------------------------------------------------- /pic/weatherType/雾.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/pic/weatherType/雾.bmp -------------------------------------------------------------------------------- /scripts/WeatherIcon.json: -------------------------------------------------------------------------------- 1 | { 2 | "多云": { 3 | "hover": "http://open.sojson.com/images/tianqi/cloud.png", 4 | "image": "http://open.sojson.com/images/tianqi/cloud2.png" 5 | }, 6 | "晴": { 7 | "hover": "http://open.sojson.com/images/tianqi/fine.png", 8 | "image": "http://open.sojson.com/images/tianqi/fine2.png" 9 | }, 10 | "阴": { 11 | "hover": "http://open.sojson.com/images/tianqi/overcast.png", 12 | "image": "http://open.sojson.com/images/tianqi/overcast2.png" 13 | }, 14 | "小雨": { 15 | "hover": "http://open.sojson.com/images/tianqi/small_rain.png", 16 | "image": "http://open.sojson.com/images/tianqi/small_rain2.png" 17 | }, 18 | "小到中雨": { 19 | "hover": "http://open.sojson.com/images/tianqi/stom_rain.png", 20 | "image": "http://open.sojson.com/images/tianqi/stom_rain2.png" 21 | }, 22 | "大雨|中到大雨": { 23 | "hover": "http://open.sojson.com/images/tianqi/big_rain.png", 24 | "image": "http://open.sojson.com/images/tianqi/big_rain2.png" 25 | }, 26 | "暴雨|大暴雨|特大暴雨|大到暴雨|暴雨到大暴雨|大暴雨到特大暴雨": { 27 | "hover": "http://open.sojson.com/images/tianqi/mbig_rain.png", 28 | "image": "http://open.sojson.com/images/tianqi/mbig_rain2.png" 29 | }, 30 | "雨夹雪": { 31 | "hover": "http://open.sojson.com/images/tianqi/rain_snow.png", 32 | "image": "http://open.sojson.com/images/tianqi/rain_snow2.png" 33 | }, 34 | "阵雪": { 35 | "hover": "http://open.sojson.com/images/tianqi/quick_snow.png", 36 | "image": "http://open.sojson.com/images/tianqi/quick_snow2.png" 37 | }, 38 | "雾": { 39 | "hover": "http://open.sojson.com/images/tianqi/fog.png", 40 | "image": "http://open.sojson.com/images/tianqi/fog2.png" 41 | }, 42 | "沙尘暴|浮尘|扬沙|强沙尘暴|雾霾": { 43 | "hover": "http://open.sojson.com/images/tianqi/sand.png", 44 | "image": "http://open.sojson.com/images/tianqi/sand2.png" 45 | }, 46 | "冻雨": { 47 | "hover": "http://open.sojson.com/images/tianqi/ice_rain.png", 48 | "image": "http://open.sojson.com/images/tianqi/ice_rain2.png" 49 | }, 50 | "中雨": { 51 | "hover": "http://open.sojson.com/images/tianqi/mid_rain.png", 52 | "image": "http://open.sojson.com/images/tianqi/mid_rain2.png" 53 | }, 54 | "雷阵雨伴有冰雹": { 55 | "hover": "http://open.sojson.com/images/tianqi/quick_rain_ice2.png" 56 | }, 57 | "阵雨": { 58 | "hover": "http://open.sojson.com/images/tianqi/quick_rain.png", 59 | "image": "http://open.sojson.com/images/tianqi/quick_rain2.png" 60 | }, 61 | "雷阵雨": { 62 | "hover": "http://open.sojson.com/images/tianqi/lquick_rain.png", 63 | "image": "http://open.sojson.com/images/tianqi/lquick_rain2.png" 64 | }, 65 | "无天气类型": { 66 | "hover": "http://open.sojson.com/images/tianqi/unknown.png", 67 | "image": "http://open.sojson.com/images/tianqi/unknown2.png" 68 | } 69 | } -------------------------------------------------------------------------------- /scripts/WeatherStation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding:utf-8 -*- 3 | #=================BiliBili日出东水=================== 4 | # 墨水屏天气台历 5 | #---------------------------------------------------- 6 | import sys 7 | import os 8 | curPath = os.path.abspath(os.path.dirname(__file__)) 9 | rootPath = os.path.split(curPath)[0] 10 | sys.path.append(rootPath + "/lib") 11 | 12 | import epd7in5_V2 13 | import epdconfig 14 | import time 15 | from PIL import Image,ImageDraw,ImageFont,ImageChops 16 | import traceback 17 | import datetime 18 | import requests 19 | import logging 20 | from O365 import Account 21 | from collections import OrderedDict 22 | import re 23 | import threading 24 | import feedparser 25 | 26 | fontSize16 = ImageFont.truetype(rootPath + '/lib/字体.ttf', 16) 27 | fontSize20 = ImageFont.truetype(rootPath + '/lib/字体.ttf', 20) 28 | fontSize25 = ImageFont.truetype(rootPath + '/lib/字体.ttf', 25) 29 | fontSize30 = ImageFont.truetype(rootPath + '/lib/字体.ttf', 30) 30 | fontSize50 = ImageFont.truetype(rootPath + '/lib/字体.ttf', 50) 31 | fontSize70 = ImageFont.truetype(rootPath + '/lib/字体.ttf', 70) 32 | 33 | oilStrTime = "" 34 | weekStr = "" 35 | oilStrWeek = "" 36 | countUpdate_1 = False 37 | countUpdate_2 = False 38 | countUpdate_3 = False 39 | countUpdate_4 = False 40 | SwitchDay = True 41 | tempArray = ["---"]*23 42 | scheduleDic = OrderedDict() 43 | 44 | def DatetimeNow(): 45 | return datetime.datetime.now() 46 | 47 | def GetO365(maxCount): 48 | global scheduleDic 49 | scheduleDic = OrderedDict() 50 | #这里填写客户端ID #API权限中的密码(第一次生成时才能看到) 51 | credentials = ('xxxxxxxxxxxxxxxxxx', 'xxxxxxxxxxxxxxxxxxxxxxxx') 52 | account = Account(credentials) 53 | schedule = account.schedule() 54 | #查询从今天开始一个月内的日历 55 | #也可以指定 datetime(2022, 5, 30) 56 | now_time =DatetimeNow() 57 | end_time = datetime.timedelta(days =30) 58 | range_time = (now_time + end_time).strftime('%Y-%m-%d') 59 | 60 | q = schedule.new_query('start').greater_equal(now_time) 61 | q.chain('and').on_attribute('end').less_equal(range_time) 62 | 63 | getSchedule = schedule.get_events(query=q, include_recurring=True) 64 | scheduleCount = 0 65 | for event in getSchedule: 66 | #获取位置 67 | locationStr = str(event.location).split(",")[0].split(":")[1].replace("'","").replace(" ","") 68 | #获取时间 69 | dateTime = event.start 70 | #获取标题 71 | subjectStr = event.subject 72 | #获取正文 73 | bodyStr = str(event.body) 74 | startIndex = bodyStr.find("body") 75 | endIndex = bodyStr.find("/body") 76 | bodyProcessStr = bodyStr[startIndex+6:endIndex-1].replace("\n","") 77 | 78 | elemDic = OrderedDict() 79 | elemDic["location"] = locationStr 80 | elemDic["dateTime"] = dateTime 81 | elemDic["subjectStr"] = subjectStr 82 | elemDic["bodyStr"] = bodyProcessStr 83 | scheduleDic[scheduleCount] = elemDic 84 | scheduleCount+=1 85 | if scheduleCount >=maxCount: 86 | break 87 | return scheduleDic 88 | 89 | def GetTime(): 90 | return(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())+" ") 91 | #获取天气 92 | def GetTemp(): 93 | try: # 连接超时,6秒,下载文件超时,7秒 94 | r = requests.get('http://t.weather.itboy.net/api/weather/city/101280701',timeout=(6,7)) 95 | r.encoding = 'utf-8' 96 | tempList = [ 97 | (r.json()['cityInfo']['city']), #城市0 98 | (r.json()['data']['shidu']), #湿度1 99 | (r.json()['data']['forecast'][0]['low']), #今日低温2 100 | (r.json()['data']['forecast'][0]['high']), #今日高温3 101 | (r.json()['data']['forecast'][0]['type']), #今日天气4 102 | (r.json()['data']['forecast'][0]['fx']), #今日风向5 103 | (r.json()['data']['forecast'][0]['fl']), #今日风级6 104 | 105 | (r.json()['data']['forecast'][1]['low']), #明日低温7 106 | (r.json()['data']['forecast'][1]['high']), #明日高温8 107 | (r.json()['data']['forecast'][1]['type']), #明日天气9 108 | (r.json()['data']['forecast'][1]['fx']), #明日风向10 109 | (r.json()['data']['forecast'][1]['fl']), #明日风级11 110 | 111 | (r.json()['data']['forecast'][2]['low']), #后日低温12 112 | (r.json()['data']['forecast'][2]['high']), #后日高温13 113 | (r.json()['data']['forecast'][2]['type']), #后日天气14 114 | (r.json()['data']['forecast'][2]['fx']), #后日风向15 115 | (r.json()['data']['forecast'][2]['fl']), #后日风级16 116 | 117 | (r.json()['data']['forecast'][3]['low']), #大后日低温17 118 | (r.json()['data']['forecast'][3]['high']), #大后日高温18 119 | (r.json()['data']['forecast'][3]['type']), #大后日天气19 120 | (r.json()['data']['forecast'][3]['fx']), #大后日风向20 121 | (r.json()['data']['forecast'][3]['fl']), #大后日风级21 122 | 123 | (r.json()['cityInfo']['updateTime']) #更新时间22 124 | ] 125 | print(GetTime() + 'UpdateWeather...ok', flush=True) 126 | except: 127 | tempList = ["---"]*23 128 | print(GetTime() + 'UpdateWeather...Fail!', flush=True) 129 | return tempList 130 | else: 131 | return tempList 132 | 133 | def UpdateWeatherIcon(tempType): #匹配天气类型图标 134 | if(tempType == "大雨" or tempType == "中到大雨"): 135 | return "大雨.bmp" 136 | elif(tempType == "暴雨" or tempType == "大暴雨" or 137 | tempType == "特大暴雨" or tempType == "大到暴雨" or 138 | tempType == "暴雨到大暴雨" or tempType == "大暴雨到特大暴雨"): 139 | return "暴雨.bmp" 140 | elif(tempType == "沙尘暴" or tempType == "浮尘" or 141 | tempType == "扬沙" or tempType == "强沙尘暴" or 142 | tempType == "雾霾"): 143 | return "沙尘暴.bmp" 144 | elif(tempType == "---"): 145 | return "无天气类型.bmp" 146 | return (tempType + ".bmp") 147 | 148 | def TodayWeek(nowWeek): 149 | if nowWeek == "0": 150 | return"星期天" 151 | elif nowWeek =="1": 152 | return"星期一" 153 | elif nowWeek =="2": 154 | return"星期二" 155 | elif nowWeek =="3": 156 | return"星期三" 157 | elif nowWeek =="4": 158 | return"星期四" 159 | elif nowWeek =="5": 160 | return"星期五" 161 | elif nowWeek =="6": 162 | return"星期六" 163 | 164 | def UpdateData(): 165 | global tempArray 166 | tempArray = GetTemp() 167 | return tempArray 168 | 169 | def UpdateTemp(timeUpdate): 170 | global oilStrWeek 171 | global tempArray 172 | global countUpdate_1 173 | global countUpdate_2 174 | global countUpdate_3 175 | global countUpdate_4 176 | strtime2 = timeUpdate.strftime('%H:%M') #时间 177 | strtime4 = timeUpdate.strftime('%w') #星期 178 | strtime5 = timeUpdate.strftime('%H') #小时 179 | 180 | if(strtime4 != oilStrWeek): #每天重置更新天气 181 | oilStrWeek = strtime4 182 | countUpdate_1 = True 183 | countUpdate_2 = True 184 | countUpdate_3 = True 185 | countUpdate_4 = True 186 | tempArray = UpdateData() 187 | print(GetTime()+'Reset Update..', flush=True) 188 | 189 | # 天气API 只有这几个点会更新,减少无用请求 190 | intTime = int(strtime5) 191 | print(GetTime() + 'Start Update Weather...', flush=True) 192 | if(countUpdate_1 and intTime == 7): 193 | tempArray = UpdateData() 194 | countUpdate_1 = False 195 | elif(countUpdate_2 and intTime == 11): 196 | tempArray = UpdateData() 197 | countUpdate_2 = False 198 | elif(countUpdate_3 and intTime == 16): 199 | tempArray = UpdateData() 200 | countUpdate_3 = False 201 | elif(countUpdate_4 and intTime == 21 ): 202 | tempArray = UpdateData() 203 | countUpdate_4 = False 204 | 205 | def ReplaceLowTemp(lowTemp): 206 | temp_L = lowTemp.replace("低温","") 207 | temp_L = temp_L.replace("℃","") 208 | temp_L = temp_L.replace(" ","") 209 | return temp_L 210 | 211 | def ReplaceHeightTemp(heighTemp): 212 | temp_H = heighTemp.replace("高温","") 213 | temp_H = temp_H.replace("℃","") 214 | temp_H = temp_H.replace(" ","") 215 | return temp_H 216 | 217 | #348 - 640 像素 居中显示 218 | #居中显示的方法 一个字宽度30像素 例如重479像素开始 多加一个字 少空 15 个像素 219 | def AlignCenter(string,scale,startPixel): 220 | charsCount = 0 221 | for s in string: 222 | charsCount += 1 223 | charsCount *= scale/2 224 | charsCount = startPixel - charsCount 225 | return charsCount 226 | 227 | def StrLenCur(text): 228 | #字母和数字占位与汉字间距不同 229 | #2个约等于一个汉字,当包含数字或字母时放宽显示数量 230 | allStrLen = len(text) 231 | numberLen = len("".join([x for x in text if x.isdigit()])) 232 | letterLen = len("".join(re.findall(r'[A-Za-z]',text))) 233 | sumLen = (numberLen + letterLen) 234 | characterLen = allStrLen - sumLen 235 | calculateLen = characterLen + int(sumLen/3) 236 | tempLen = (int)(sumLen/3) + 12 237 | if(calculateLen >= 12): 238 | return text[0:tempLen]+"..." 239 | else: 240 | return text 241 | 242 | def DrawHorizontalDar(draw,Himage,timeUpdate): 243 | strtime = timeUpdate.strftime('%Y-%m-%d') #年月日 244 | strtime2 = timeUpdate.strftime('%H:%M') #时间 245 | strtimeW = timeUpdate.strftime('%w') #星期 246 | 247 | #显示星期 248 | draw.text((20, 15), TodayWeek(strtimeW), font = fontSize30, fill = 0) 249 | #显示时间 250 | draw.text((128, 8), strtime2, font = fontSize50, fill = 0) 251 | #显示年月日 252 | draw.text((20, 55), strtime, font = fontSize16, fill = 0) 253 | #显示城市 254 | #draw.text((220, 55), tempArray[0], font = fontSize16, fill = 0) 255 | #天气图标 256 | tempTypeIcon = Image.open(rootPath + "/pic/weatherType/" + UpdateWeatherIcon(tempArray[4])) 257 | Himage.paste(tempTypeIcon,(280,18)) 258 | #今日天气 259 | draw.text((330,18),tempArray[4], font = fontSize25, fill = 0) 260 | #温度 261 | todayTemp = ReplaceLowTemp(tempArray[2])+"-"+ReplaceHeightTemp(tempArray[3]) +" 度" 262 | draw.text((450,55),todayTemp, font = fontSize16, fill = 0) 263 | #温度图标 264 | TempIcon = Image.open(rootPath + "/pic/temp.png") 265 | Himage.paste(TempIcon,(450,15)) 266 | #显示湿度 267 | draw.text((545,55),"湿度: "+ tempArray[1], font = fontSize16, fill = 0) 268 | #湿度图标 269 | tmoistureIcon = Image.open(rootPath + "/pic/moisture.png") 270 | Himage.paste(tmoistureIcon,(545,15)) 271 | #风力 272 | windTemp = tempArray[5] + tempArray[6] 273 | draw.text((330,55),windTemp, font = fontSize16, fill = 0) 274 | 275 | def DrawSchedule(draw,timeUpdate): 276 | global scheduleDic 277 | if(len(scheduleDic) <= 0): 278 | elemDic = OrderedDict() 279 | elemDic["location"] = "" 280 | elemDic["dateTime"] = DatetimeNow() 281 | elemDic["subjectStr"] = "暂无日程..." 282 | elemDic["bodyStr"] = "" 283 | scheduleDic[0] = elemDic 284 | for x in range(0,len(scheduleDic)): 285 | t = scheduleDic[x]["dateTime"] 286 | subjectStr = scheduleDic[x]["subjectStr"] 287 | #bodyStr = scheduleDic[x]["bodyStr"] 288 | #黑方框标记今日 289 | fillColor = 0 290 | if(int(t.day) == int(timeUpdate.strftime('%d'))): 291 | draw.rectangle((5, 95 + x*50, 80, 135 + x*50), fill = "black") 292 | fillColor = 255 293 | draw.text((10,100 + x*50),str(t.month) +"月"+ str(t.day)+"日", font = fontSize16, fill = fillColor) 294 | draw.text((10,115 + x*50),str(t.strftime('%H:%M')), font = fontSize16, fill = fillColor) 295 | #日程标题 296 | draw.text((90,95 + x*50),StrLenCur(str(subjectStr)), font = fontSize25, fill = 0) 297 | #draw.text((15,80 + x*100),bodyStr, font = fontSize16, fill = 0) 298 | 299 | def DrawRss(draw): 300 | global scheduleDic 301 | if(len(scheduleDic) <= 0): 302 | elemDic = OrderedDict() 303 | elemDic["location"] = "" 304 | elemDic["dateTime"] = DatetimeNow() 305 | elemDic["subjectStr"] = "暂无日程..." 306 | elemDic["bodyStr"] = "" 307 | scheduleDic[0] = elemDic 308 | for x in range(0,len(scheduleDic)): 309 | subjectStr = scheduleDic[x]["subjectStr"] 310 | #rss标题 311 | draw.text((10,95 + x*50),StrLenCur(str(subjectStr)), font = fontSize25, fill = 0) 312 | 313 | def GetRss(): 314 | global scheduleDic 315 | while(True): 316 | d = feedparser.parse("https://www.solidot.org/index.rss") 317 | intLen = len(d["entries"]) 318 | 319 | for i in range(0,intLen): 320 | elemDic = OrderedDict() 321 | elemDic["location"] = "" 322 | elemDic["dateTime"] = DatetimeNow() 323 | elemDic["subjectStr"] = d["entries"][i]["title"] 324 | elemDic["bodyStr"] = "" 325 | scheduleDic[i] = elemDic 326 | 327 | timeUpdate = DatetimeNow() 328 | UpdateTemp(timeUpdate) 329 | strtime5 = timeUpdate.strftime('%H') 330 | intTime = int(strtime5) 331 | if(intTime >= 1 and intTime <= 6): #2点~6点 每2小时刷新一次 332 | time.sleep(7200) 333 | else: 334 | time.sleep(3600) 335 | 336 | def NetworkThreading(): 337 | global scheduleDic 338 | while(True): 339 | timeUpdate = DatetimeNow() 340 | UpdateTemp(timeUpdate) 341 | try: 342 | print(GetTime() + 'Start Update Schedule...', flush=True) 343 | scheduleDic = GetO365(6) 344 | print(GetTime() + 'Update Schedule ok!', flush=True) 345 | except: 346 | elemDic = OrderedDict() 347 | elemDic["location"] = "" 348 | elemDic["dateTime"] = DatetimeNow() 349 | elemDic["subjectStr"] = "日程获取失败!稍后重试.." 350 | elemDic["bodyStr"] = "" 351 | scheduleDic[0] = elemDic 352 | print(GetTime() + 'Update Schedule Fail!', flush=True) 353 | 354 | strtime5 = timeUpdate.strftime('%H') 355 | intTime = int(strtime5) 356 | if(intTime >= 1 and intTime <= 6): #2点~6点 每小时刷新一次 357 | time.sleep(3600) 358 | else: 359 | time.sleep(600) 360 | 361 | def WeatherStrSwitch(index): 362 | if index == 0: 363 | return"明天" 364 | elif index == 1: 365 | return"后天" 366 | elif index == 2: 367 | return"大后天" 368 | 369 | def WeatherSwitch(index): 370 | if index == 0: 371 | return 4 372 | elif index == 1: 373 | return 9 374 | elif index == 2: 375 | return 14 376 | 377 | def DrawWeather(draw,Himage): 378 | for x in range(0,3): 379 | draw.text((430,90 + x *100),WeatherStrSwitch(x), font = fontSize16, fill = 0) 380 | strWeather = tempArray[WeatherSwitch(x)] 381 | #图标 382 | pathIcon = UpdateWeatherIcon(strWeather) 383 | tempTypeIcon = Image.open(rootPath + "/pic/weatherType/" + pathIcon) 384 | Himage.paste(tempTypeIcon,(454,112 + x*100)) 385 | #天气 386 | draw.text((510,120 + x *100),strWeather, font = fontSize20, fill = 0) 387 | #温度 388 | forecastTemp = ReplaceLowTemp(tempArray[WeatherSwitch(x)-2])+"-"+ReplaceHeightTemp(tempArray[WeatherSwitch(x)-1]) +" 度" 389 | draw.text((500,145 + x *100),forecastTemp, font = fontSize20, fill = 0) 390 | #风力 391 | windTemp = tempArray[WeatherSwitch(x)+1] + tempArray[WeatherSwitch(x)+2] 392 | draw.text((555,90 + x *100),windTemp, font = fontSize16, fill = 0) 393 | 394 | def UpdateTime(): 395 | #刷新循环 396 | while (True): 397 | print(GetTime()+'Epd7in5 Init...', flush=True) 398 | epd = epd7in5_V2.EPD() 399 | epd.init() 400 | epd.Clear() 401 | print(GetTime()+'Epd7in5 Init ok!', flush=True) 402 | timeUpdate = DatetimeNow() 403 | #时间 404 | strtime2 = timeUpdate.strftime('%H:%M') 405 | #小时 406 | strtime5 = timeUpdate.strftime('%H') 407 | intTime = int(strtime5) 408 | #新建空白图片 409 | Himage = Image.new('1', (640, 384), 255) 410 | #缩放图片 411 | Himage = Himage.resize((epd.width,epd.height),resample=Image.NEAREST) 412 | 413 | draw = ImageDraw.Draw(Himage) 414 | 415 | #显示背景 416 | bmp = Image.open(rootPath + '/pic/bg.png') 417 | Himage.paste(bmp,(0,0)) 418 | 419 | #绘制水平栏 420 | DrawHorizontalDar(draw,Himage,timeUpdate) 421 | 422 | #绘制日程 423 | #DrawSchedule(draw,timeUpdate) 424 | DrawRss(draw) 425 | 426 | #绘制天气预报 427 | DrawWeather(draw,Himage) 428 | 429 | #画线(x开始值,y开始值,x结束值,y结束值) 430 | #draw.rectangle((280, 90, 280, 290), fill = 0) 431 | 432 | #刷新屏幕 433 | print(GetTime() + 'Start Update Screen...', flush=True) 434 | #反向图片 435 | #Himage = ImageChops.invert(Himage) 436 | #Himage.save(rootPath +"/test.png") 437 | #bmp = Image.open(rootPath + '/test.png') 438 | 439 | epd.display(epd.getbuffer(Himage)) 440 | #屏幕休眠 441 | print(GetTime() + 'Update Screen ok!', flush=True) 442 | epd.sleep() 443 | 444 | print(GetTime() + 'Screen Sleep...', flush=True) 445 | if(intTime >= 1 and intTime <= 6): #2点~6点 每小时刷新一次 446 | time.sleep(3600) 447 | else: 448 | time.sleep(600) 449 | 450 | #networkThreading = threading.Thread(target=NetworkThreading, args=()) 451 | #networkThreading.start() 452 | networkGetRss = threading.Thread(target=GetRss, args=()) 453 | networkGetRss.start() 454 | timeThreading = threading.Thread(target=UpdateTime, args=()) 455 | timeThreading.start() 456 | 457 | -------------------------------------------------------------------------------- /scripts/天气模板.json: -------------------------------------------------------------------------------- 1 | { 2 | "time": "2019-07-22 10:08:24", 3 | "cityInfo": { 4 | "city": "珠海市", 5 | "cityId": "101280701", 6 | "parent": "广东", 7 | "updateTime": "09:54" 8 | }, 9 | "date": "20190722", 10 | "message": "Success !", 11 | "status": 200, 12 | "data": { 13 | "shidu": "75%", 14 | "pm25": 9.0, 15 | "pm10": 16.0, 16 | "quality": "优", 17 | "wendu": "30", 18 | "ganmao": "各类人群可自由活动", 19 | "yesterday": { 20 | "date": "21", 21 | "sunrise": "05:52", 22 | "high": "高温 32.0℃", 23 | "low": "低温 27.0℃", 24 | "sunset": "19:11", 25 | "aqi": 19.0, 26 | "ymd": "2019-07-21", 27 | "week": "星期日", 28 | "fx": "无持续风向", 29 | "fl": "<3级", 30 | "type": "中雨", 31 | "notice": "记得随身携带雨伞哦" 32 | }, 33 | "forecast": [{ 34 | "date": "22", 35 | "sunrise": "05:53", 36 | "high": "高温 32.0℃", 37 | "low": "低温 26.0℃", 38 | "sunset": "19:11", 39 | "aqi": 20.0, 40 | "ymd": "2019-07-22", 41 | "week": "星期一", 42 | "fx": "无持续风向", 43 | "fl": "<3级", 44 | "type": "中雨", 45 | "notice": "记得随身携带雨伞哦" 46 | }, { 47 | "date": "23", 48 | "sunrise": "05:53", 49 | "high": "高温 31.0℃", 50 | "low": "低温 26.0℃", 51 | "sunset": "19:10", 52 | "aqi": 52.0, 53 | "ymd": "2019-07-23", 54 | "week": "星期二", 55 | "fx": "南风", 56 | "fl": "3-4级", 57 | "type": "雷阵雨", 58 | "notice": "带好雨具,别在树下躲雨" 59 | }, { 60 | "date": "24", 61 | "sunrise": "05:53", 62 | "high": "高温 31.0℃", 63 | "low": "低温 27.0℃", 64 | "sunset": "19:10", 65 | "aqi": 49.0, 66 | "ymd": "2019-07-24", 67 | "week": "星期三", 68 | "fx": "无持续风向", 69 | "fl": "<3级", 70 | "type": "雷阵雨", 71 | "notice": "带好雨具,别在树下躲雨" 72 | }, { 73 | "date": "25", 74 | "sunrise": "05:54", 75 | "high": "高温 32.0℃", 76 | "low": "低温 26.0℃", 77 | "sunset": "19:10", 78 | "aqi": 47.0, 79 | "ymd": "2019-07-25", 80 | "week": "星期四", 81 | "fx": "无持续风向", 82 | "fl": "<3级", 83 | "type": "多云", 84 | "notice": "阴晴之间,谨防紫外线侵扰" 85 | }, { 86 | "date": "26", 87 | "sunrise": "05:54", 88 | "high": "高温 33.0℃", 89 | "low": "低温 27.0℃", 90 | "sunset": "19:09", 91 | "aqi": 48.0, 92 | "ymd": "2019-07-26", 93 | "week": "星期五", 94 | "fx": "无持续风向", 95 | "fl": "<3级", 96 | "type": "多云", 97 | "notice": "阴晴之间,谨防紫外线侵扰" 98 | }, { 99 | "date": "27", 100 | "sunrise": "05:55", 101 | "high": "高温 33.0℃", 102 | "low": "低温 27.0℃", 103 | "sunset": "19:09", 104 | "aqi": 49.0, 105 | "ymd": "2019-07-27", 106 | "week": "星期六", 107 | "fx": "无持续风向", 108 | "fl": "<3级", 109 | "type": "阵雨", 110 | "notice": "阵雨来袭,出门记得带伞" 111 | }, { 112 | "date": "28", 113 | "sunrise": "05:55", 114 | "high": "高温 32.0℃", 115 | "low": "低温 26.0℃", 116 | "sunset": "19:09", 117 | "ymd": "2019-07-28", 118 | "week": "星期日", 119 | "fx": "无持续风向", 120 | "fl": "<3级", 121 | "type": "阵雨", 122 | "notice": "阵雨来袭,出门记得带伞" 123 | }, { 124 | "date": "29", 125 | "sunrise": "05:55", 126 | "high": "高温 29.0℃", 127 | "low": "低温 27.0℃", 128 | "sunset": "19:08", 129 | "ymd": "2019-07-29", 130 | "week": "星期一", 131 | "fx": "南风", 132 | "fl": "3-4级", 133 | "type": "小雨", 134 | "notice": "雨虽小,注意保暖别感冒" 135 | }, { 136 | "date": "30", 137 | "sunrise": "05:56", 138 | "high": "高温 29.0℃", 139 | "low": "低温 27.0℃", 140 | "sunset": "19:08", 141 | "ymd": "2019-07-30", 142 | "week": "星期二", 143 | "fx": "南风", 144 | "fl": "3-4级", 145 | "type": "小雨", 146 | "notice": "雨虽小,注意保暖别感冒" 147 | }, { 148 | "date": "31", 149 | "sunrise": "05:56", 150 | "high": "高温 29.0℃", 151 | "low": "低温 27.0℃", 152 | "sunset": "19:07", 153 | "ymd": "2019-07-31", 154 | "week": "星期三", 155 | "fx": "南风", 156 | "fl": "<3级", 157 | "type": "小雨", 158 | "notice": "雨虽小,注意保暖别感冒" 159 | }, { 160 | "date": "01", 161 | "sunrise": "05:57", 162 | "high": "高温 30.0℃", 163 | "low": "低温 28.0℃", 164 | "sunset": "19:07", 165 | "ymd": "2019-08-01", 166 | "week": "星期四", 167 | "fx": "西南风", 168 | "fl": "<3级", 169 | "type": "小雨", 170 | "notice": "雨虽小,注意保暖别感冒" 171 | }, { 172 | "date": "02", 173 | "sunrise": "05:57", 174 | "high": "高温 30.0℃", 175 | "low": "低温 28.0℃", 176 | "sunset": "19:06", 177 | "ymd": "2019-08-02", 178 | "week": "星期五", 179 | "fx": "南风", 180 | "fl": "<3级", 181 | "type": "小雨", 182 | "notice": "雨虽小,注意保暖别感冒" 183 | }, { 184 | "date": "03", 185 | "sunrise": "05:57", 186 | "high": "高温 30.0℃", 187 | "low": "低温 27.0℃", 188 | "sunset": "19:06", 189 | "ymd": "2019-08-03", 190 | "week": "星期六", 191 | "fx": "南风", 192 | "fl": "<3级", 193 | "type": "小雨", 194 | "notice": "雨虽小,注意保暖别感冒" 195 | }, { 196 | "date": "04", 197 | "sunrise": "05:58", 198 | "high": "高温 30.0℃", 199 | "low": "低温 27.0℃", 200 | "sunset": "19:05", 201 | "ymd": "2019-08-04", 202 | "week": "星期日", 203 | "fx": "南风", 204 | "fl": "<3级", 205 | "type": "小雨", 206 | "notice": "雨虽小,注意保暖别感冒" 207 | }, { 208 | "date": "05", 209 | "sunrise": "05:58", 210 | "high": "高温 30.0℃", 211 | "low": "低温 27.0℃", 212 | "sunset": "19:04", 213 | "ymd": "2019-08-05", 214 | "week": "星期一", 215 | "fx": "南风", 216 | "fl": "<3级", 217 | "type": "小雨", 218 | "notice": "雨虽小,注意保暖别感冒" 219 | }] 220 | } 221 | } -------------------------------------------------------------------------------- /startWeather.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sudo python3 /home/pi/RaspberryPi-WeatherStation/scripts/WeatherStation.py -------------------------------------------------------------------------------- /设计界面.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShaderFallback/Outlook_CalendarWeather_ePaper/887fcf28b378695bd340342aad2dc0f43ca7cc08/设计界面.psd --------------------------------------------------------------------------------